Prerequisite: Your organization must be approved before it can quote, and the wallet must hold the asset it sells. See KYC Overview for what verification requires, and KYC WebSDK for the hosted flow.
Redemptions do not depend on how much was onramped. A customer can sell any amount of a stablebond up to their current balance. We do not pair buys with sells or track lots, so partial redemptions and tokens bought elsewhere both work.
$ORGANIZATION_ID in the examples. Use your own organization’s id from GET /me, or a customer organization’s id when you ramp for that customer. Register the accounts, quote and order under the same id. See Organizations in the route.
export ORGANIZATION_ID=<organization_id>
Offramp flow
1
Discover assets
Show Details
Show Details
Either of these endpoints gives you the asset identifiers for a quote:
- GET /supported_assets requires auth. It returns every market and token a quote can use. Pass
paymentAccountIdto see what one of your accounts can use, orchainfor one chain. - GET /lookup/stablebonds is public. It returns every stablebond on every chain, with pricing and supply data.
asset from the first, or the identifier from the second, as sourceAsset in the quote.Asset identifier format differs by chain:
- Solana: Base58 mint address (for example
AvvetPGuuB5FD5m86fpw3LtDKyQoUFT1mG9WarNQLW4q) - Stellar:
CODE:ISSUER(for exampleCETES:GC3CW7...) - EVM (Base, Polygon, Monad, Avalanche): the raw contract address only (for example
0xcC77c598d42f2f78Beb42C91d12B9d4041a5cE29)
SYMBOL:0x... identifier on an EVM chain fails.2
Register the wallet and the bank account: POST /organization/{organizationId}/payment_account
Show Details
Show Details
Register the wallet that sends the crypto and the bank account that receives the payout. Each call returns the account’s For an offramp, the wallet needs
id and its rails. See Register payment account for the full schema.curl -X POST https://api.sand.etherfuse.com/organization/$ORGANIZATION_ID/payment_account \
-H "Authorization: <api_key>" \
-H "Content-Type: application/json" \
-d '{
"kind": "wallet",
"label": "Treasury wallet",
"accountDetails": {
"kind": "address",
"address": "0xf82567432381D1326484E5A9140ABCa4294f82D0",
"chain": "base"
}
}'
curl -X POST https://api.sand.etherfuse.com/organization/$ORGANIZATION_ID/payment_account \
-H "Authorization: <api_key>" \
-H "Content-Type: application/json" \
-d '{
"kind": "bank",
"label": "Operating account",
"country": "MX",
"currency": "MXN",
"accountDetails": { "kind": "clabe", "clabe": "012180000000000002" }
}'
source ready on its chain rail, and the bank account needs destination ready on its fiat rail (spei). Read the account again with GET /organization/{organizationId}/payment_account/{id}, or listen for paymentAccountUpdated.A bank account route needs holder details such as
accountHolderName and taxId. We fill them from the verified identity of the organization in the route, so pass the customer’s organization id to register a customer’s bank account. If a field is still missing, we return 422 registration-info-required with the fields in missing. Send them under registrationInfo and retry.3
Create a quote: POST /organization/{organizationId}/quote
Show Details
Show Details
Quote from the wallet to the bank account. Both accounts must belong to the organization in the route. Set
sourceAsset to the asset identifier and destinationAsset to the fiat currency. Quotes expire 2 minutes after they are created (expiresAt). See Create quote for the full schema.curl -X POST https://api.sand.etherfuse.com/organization/$ORGANIZATION_ID/quote \
-H "Authorization: <api_key>" \
-H "Content-Type: application/json" \
-d '{
"source": { "type": "stored", "id": "<wallet_payment_account_id>" },
"destination": { "type": "stored", "id": "<bank_payment_account_id>" },
"sourceAmount": "50",
"sourceAsset": "0xcC77c598d42f2f78Beb42C91d12B9d4041a5cE29",
"destinationAsset": "MXN"
}'
curl -X POST https://api.etherfuse.com/organization/$ORGANIZATION_ID/quote \
-H "Authorization: <api_key>" \
-H "Content-Type: application/json" \
-d '{
"source": { "type": "stored", "id": "<wallet_payment_account_id>" },
"destination": { "type": "stored", "id": "<bank_payment_account_id>" },
"sourceAmount": "50",
"sourceAsset": "0x834df4C1d8f51Be24322E39e4766697BE015512F",
"destinationAsset": "MXN"
}'
Partner fees: We apply your organization’s partner fee automatically. To set it for one quote, add
"partnerFeeBpsOverride": 100. We cap values above 500. See Partner fees.4
Create the order: POST /organization/{organizationId}/order
Show Details
Show Details
Send the On an offramp,
quoteId. The response is 201 with the full order, and the order’s id equals the quoteId. Sending the same quoteId again returns the existing order with 200. See Create order for the full schema.curl -X POST https://api.sand.etherfuse.com/organization/$ORGANIZATION_ID/order \
-H "Authorization: <api_key>" \
-H "Content-Type: application/json" \
-d '{ "quoteId": "<quote_id>" }'
curl -X POST https://api.etherfuse.com/organization/$ORGANIZATION_ID/order \
-H "Authorization: <api_key>" \
-H "Content-Type: application/json" \
-d '{ "quoteId": "<quote_id>" }'
fundingInstructions.rail is the wallet’s chain, and fundingInstructions.destination says where to send the crypto:{
"fundingInstructions": {
"rail": "stellar",
"reference": "1000043",
"destination": {
"expiresAt": null,
"address": "GABPM7AXX...",
"memo": "<memo>",
"preparedTransaction": {
"transaction": "AAAAAgAAAABk...base64_xdr...",
"refreshAfter": "2026-09-28T18:05:02Z"
}
},
"fundingDeadline": "2026-09-28T18:05:02Z"
}
}
| Field | Meaning |
|---|---|
address | The address to send the crypto to. |
memo | The memo to include in the transfer. Absent when none is needed. |
preparedTransaction | A transaction we built for the customer to sign and submit. Absent when there is none. |
preparedTransaction.refreshAfter | After this time the transaction can no longer land. On Solana it is about 60 seconds after we build it, when the blockhash expires. On Stellar it is the order’s fundingDeadline. Null on EVM chains, where the transaction has no time limit. A Stellar or EVM transaction also goes stale when the wallet’s sequence number or nonce moves. |
destination is absent until it is available. You receive it in an orderUpdated event, or you can read the order again.5
Send the crypto
Show Details
Show Details
Offramps need an on-chain action from the customer. Fund the order before
fundingDeadline in one of two ways:- Sign the prepared transaction. When
preparedTransactionis present, deserializetransaction, sign it with the customer’s wallet, and submit it to the network. - Send a transfer yourself. Send the order’s
source.amountofsource.assettoaddress, withmemowhen it is present.
Prepared transactions go stale. A Solana transaction goes stale after
refreshAfter, when its blockhash expires. A Stellar or EVM transaction goes stale when the wallet sends another transaction first, because its sequence number or nonce moves. In either case, call POST /organization/{organizationId}/order/{id}/funding_instructions/refresh. The new funding instructions arrive a moment later in an orderUpdated webhook and on GET. See Stellar.Embedded wallets sign through their owner instead. See Embedded Wallets.
6
Track the order: GET /organization/{organizationId}/order/{id}
Show Details
Show Details
We detect the transfer on-chain and send the fiat payout. The status moves
created → funded (crypto received) → completed (fiat sent). Read the order, or subscribe to orderUpdated with POST /webhook_subscription. See Get order for the response schema.No fiat simulation. Offramps have no
fiat_received step. Sending the crypto on the testnet is enough.curl -H "Authorization: <api_key>" \
https://api.sand.etherfuse.com/organization/$ORGANIZATION_ID/order/<order_id>
curl -H "Authorization: <api_key>" \
https://api.etherfuse.com/organization/$ORGANIZATION_ID/order/<order_id>
Show Chain-specific quote examples
Show Chain-specific quote examples
The destination is always the bank account. Only the wallet and Stellar: use Base, Polygon, Monad or Avalanche: use the raw contract address. This example uses Base; get the address for your chain from
sourceAsset change.Solana: use the Base58 mint address:{
"source": { "type": "stored", "id": "<solana_wallet_payment_account_id>" },
"destination": { "type": "stored", "id": "<bank_payment_account_id>" },
"sourceAmount": "50",
"sourceAsset": "AvvetPGuuB5FD5m86fpw3LtDKyQoUFT1mG9WarNQLW4q",
"destinationAsset": "MXN"
}
CODE:ISSUER:{
"source": { "type": "stored", "id": "<stellar_wallet_payment_account_id>" },
"destination": { "type": "stored", "id": "<bank_payment_account_id>" },
"sourceAmount": "50",
"sourceAsset": "CETES:GC3CW7...",
"destinationAsset": "MXN"
}
GET /supported_assets:{
"source": { "type": "stored", "id": "<evm_wallet_payment_account_id>" },
"destination": { "type": "stored", "id": "<bank_payment_account_id>" },
"sourceAmount": "50",
"sourceAsset": "0xcC77c598d42f2f78Beb42C91d12B9d4041a5cE29",
"destinationAsset": "MXN"
}
Show Which assets each chain supports
Show Which assets each chain supports
| Source asset | Chains |
|---|---|
| Stablebond (for example CETES) | Solana, Stellar, Base, Polygon, Monad, Avalanche |
| Stablecoin (for example USDC) | Solana, Stellar, Monad |
bridge fee line.Show Common errors
Show Common errors
| Error | Cause | Fix |
|---|---|---|
409 organization-not-approved | Your organization is not approved | Complete verification, then quote again |
409 payment-account-not-ready | The wallet is not ready as a source, or the bank account is not ready as a destination | Check each account’s rails and wait for the direction to become ready |
400 quote-expired | The quote is older than 2 minutes, or does not exist | Create a new quote |
400 amount-out-of-range | The amount is outside a rail’s minAmount or maxAmount | Change the amount |
422 quote-unavailable | We cannot price the route right now | Retry with backoff |
| Quote rejected for a stablecoin on Base or Polygon | We quote stablecoins only on Solana, Stellar and Monad | Sell a stablebond, or use a wallet on one of those chains |
tx_bad_seq (Stellar), a nonce error (EVM), or an expired blockhash (Solana) | The prepared transaction went stale before submission | Refresh the funding instructions and sign the new transaction |
| Stellar transfer fails | The wallet does not hold enough XLM for the network fee | Fund the wallet with XLM. See Stellar |