Skip to Content
XebraDocs
Guides

Claim a stuck transfer

Finish a delivery that did not complete on its own, from the claim page or through the API.

Circle's Forwarding Service mints almost every transfer on its own. When it does not, the burn is still safe: Circle has attested it, and that attestation lets anyone submit the mint on the destination. The mint always pays the recipient the burn names. Whoever submits it only pays the destination's network fee.

A transfer needs a claim when tracking shows needs_claim or failed. For why this happens and who can act, see Recovery.

Two ways to finish it

  1. Send the user to claimUrl. For transfers from Stellar and EVM chains, the status carries a link to the claim page on xebra.exchange. The recipient connects a wallet on the destination and mints. This is the simplest path and needs no code.
  2. Build the mint through the API. POST /v1/claims returns the unsigned mint for a wallet you control. Use this to finish transfers in your own app, or for Sui transfers, which have no claim link.

Through the API

Call createClaim with:

FieldValue
chainThe chain the burn was made on (the source)
txThe burn's hash or Sui digest, as you track it
payerThe address on the destination that signs and pays the fee
recipientOwnerSolana only, when asked for: the wallet that owns the recipient's USDC account

What comes back depends on the destination:

DestinationStepstx.kind
An EVM chainclaim: Circle's receiveMessage on that chainevm
Solanaopen_account when the recipient's USDC account does not exist yet, then claimsolana
Stellar (from Sui)claim: the mint through Circle's forwarder on Stellarstellar

The claim is checked before it is returned, so nobody pays a fee for a mint that cannot work:

  • not_ready: Circle has not attested the burn yet. Ask again in a few minutes.
  • already_delivered: the mint has already happened. Nothing to do.
  • transaction_rejected: the mint would revert. For a Fast transfer this can mean Circle's signature has expired; the claim page on xebra.exchange can have it signed again.
  • invalid_request: the payer is not an address on the destination, or this destination cannot be claimed through the API. On Solana, a problem whose needs field is "recipientOwner" means you must pass recipientOwner.

In code

import { ,  } from "@xebra/sdk";

const  = ();
declare const : string;
// Your Solana wallet and RPC: sign, send, wait for confirmation, return the signature.
declare function (: string): <string>;

const  = { : "stellar", : , : "PayerSolanaWallet..." } as ;

try {
  let {  } = await .();
  const  = .(() => . === "open_account");
  if (?.. === "solana") {
    await (..); // confirmed before going on
    ({  } = await .()); // a fresh mint, now that the account exists
  }
  const  = .(() => . === "claim");
  if (?.. !== "solana") throw new ("expected a Solana claim");
  await (..);
} catch () {
  if ( instanceof  && . === "already_delivered") {
    // Nothing to do: the USDC has arrived.
  } else if ( instanceof  && . === "not_ready") {
    // Circle has not attested the burn yet. Ask again in a few minutes.
  } else if ( instanceof  && ..needs === "recipientOwner") {
    // Solana: pass recipientOwner, the wallet that owns the recipient's USDC account.
  } else {
    throw ;
  }
}

Solana: two transactions

When the recipient has no USDC account on Solana, the claim has two steps. open_account creates the account; it costs the payer a small amount of SOL as rent, which is not returned. Confirm it before the next step.

Each Solana tx.transaction is a legacy transaction, serialized without signatures, in base64, with the payer as fee payer and a recent blockhash already set. A recent blockhash is valid only briefly, and a wallet's simulation of the mint fails while the account it mints into does not exist. So when there is an open_account step: sign and confirm it, then call createClaim again. The second answer has only the claim step, built fresh.

EVM: one transaction

On an EVM destination the claim is one transaction to Circle's MessageTransmitter, value: "0". Switch the wallet to tx.chainId and send it. The payer pays gas on the destination chain.

On this page