Skip to main content
This recipe pays fiat to a third party, such as a supplier, a seller or one of your users, and funds the payout with stablecoins. You deposit USDT or USDC into the merchant’s VelaFi wallet, register the recipient’s bank account as a beneficiary, and create a crypto_to_fiat order for each payout. VelaFi converts the crypto at the quoted rate and sends the fiat through the local rail (for example SPEI, Pix, CVU, Bre-B) or through Circle Payment Network (CPN) for USD and EUR. For the full field reference, see the Off-Ramp guide and Create an Off-Ramp Order.

How it works

Before you start

  • API access. An API key, your server IPs on the whitelist, and a business token for the X-BH-TOKEN header. See Authentication.
  • A verified merchant with an activated account for the payout currency and channel. The beneficiary must use a payment method (paymentId) of that account. See Activate a Merchant Account.
  • ORDER_WEBHOOK registered. Your endpoint must return HTTP 2xx with the body {"callbackStatus":"SUCCESS"}, or the delivery counts as failed. See Webhooks and Delivery and Retries.

Build the flow

1

Fund the merchant's crypto wallet

Get the merchant’s deposit address for the token and network you will send:
Send the crypto to address (with addressExt if needAddressTag is true), at least minQuantity. See Get Crypto Deposit Address for the supported token and network pairs.There is no webhook for incoming crypto deposits. Confirm the credit with Get Crypto Deposit History (status 2 is successful) and check the spendable amount in free from Get Crypto Balance.
2

Register the beneficiary

Get the field template for the payout method with GET /v2/payments/templates?paymentId={paymentId}, then submit the filled-in fields with POST /v2/payments. Use the same merchantId as your orders.
Response
Store data.id. It is the userPaymentId for your orders. Only a beneficiary in status 1 (valid) can receive payouts. 2 means verification is still in progress. 3 means it failed: read failReason, and if the account already exists, use sameAccountNumberId instead of creating a new record. Register a beneficiary once and reuse it. See Manage Beneficiaries.
3

Optional: lock the rate with a quote

Call Get a Crypto/Fiat Quote with from set to the crypto, to set to the fiat and createQuoteId=true. The price is fiat per 1 unit of crypto.
A quoteId is valid for 10 seconds in production (300 seconds in the sandbox), is single-use, and is bound to the country, crypto and fiat it was issued for. Without a quoteId, the order is priced when it is created.
4

Create the payout order

Call POST /v2/order/crypto_to_fiat with the beneficiary’s userPaymentId. Use the country and fiat of the beneficiary. Send fiatAmount to fix what the beneficiary receives, or cryptoAmount to fix what you spend. If you send both, fiatAmount wins. Always send a unique clientId.
Response
How the order is funded. The crypto comes from the merchant’s wallet balance for that crypto. VelaFi debits it when it creates the order, and the order moves to 40 (or 10, 11 or 31 first when review or documents are required). If the balance is too low, the request is rejected with an insufficient-balance error and no order is created.Post-funded variant. If VelaFi has enabled post-funded off-ramp for your account (it can differ by fiat currency), an underfunded order is accepted and waits in 32 (Awaiting crypto deposit). Order detail then returns orderExpireTimestamp, the deadline in Unix seconds. Deposit enough crypto to the wallet before the deadline. VelaFi checks the balance every minute, applies it to waiting orders oldest first, and moves a covered order to 40. If the deadline passes first, the order is cancelled (70). See Funding the order.If the request times out, look the order up by clientId before you retry. See Idempotency and retries.
5

Track the payout to completion

Wait for ORDER_WEBHOOK events (orderType is SELL), or poll GET /v2/order/detail?orderType=crypto_to_fiat&clientId={clientId}.traceNumber holds the central bank reference number for the payout when the rail returns one. Share it with the beneficiary if they ask for proof of payment.

USD and EUR payouts through Circle Payment Network

CPN carries USD and EUR payouts. It is payout only: you cannot collect funds or create fiat_to_crypto orders on CPN.
  1. Activate a CPN account for the merchant and currency with trench set to Circle Payment Network. See Activate a Merchant Account for a Business.
  2. Register the beneficiary on a CPN payment method, for example 120 Wire (CPN) or 127 FEDWIRE (CPN) for USD and 124 SEPA (CPN) for EUR. CPN templates ask for the beneficiary’s address, a useCase and a reasonForPayment code. See the CPN templates in Get Payment Templates.
  3. Create the crypto_to_fiat order with fiat set to USD or EUR and the CPN beneficiary’s userPaymentId, exactly as above.
Like any payout, a CPN order can go into risk review (11). When it does, upload the requested documents with Upload Documents. The order then moves to 12 and waits for a decision.

Webhooks to handle

ORDER_WEBHOOK is sent only for orders created through the API. It does not send 71 (Cancelling) or 73 (Refunding), and orders rejected in review or risk review are sent as 70. Events can arrive more than once and out of order, and intermediate statuses can be skipped. Deduplicate on orderId + orderStatus, and read the order with GET /v2/order/detail when an event does not fit the state you hold.

Handle failures

failReason is often empty, because many rails return no reason. Treat an empty failReason as “unknown”, not as success. To avoid paying twice, only create a replacement payout after the original order is final (70 or 72).

Reconcile

  • Each payout: Get Order by orderId or clientId is the source of truth: orderStatus, cryptoAmount (debited from the wallet), fiatAmount (paid to the beneficiary), fiatFee, orderPrice, orderFeeDetail, traceNumber, failCode and failReason.
  • Daily totals: List Orders with orderType=crypto_to_fiat and a time range. It also lists orders not created through the API.
  • Wallet: Get Crypto Deposit History for top-ups, and Get Crypto Balance for free and locked.
Run a scheduled job that re-reads every payout still open on your side, and flag orders that stay in 40 or 50 beyond your normal payout time. Webhooks can be missed after three failed delivery attempts.

Test in the sandbox

  • Use https://api-test.velafi.com. Webhooks are not sent in the sandbox, so poll GET /v2/order/detail.
  • Add test USDT or USDC to a merchant wallet with the crypto deposit simulator. See Sandbox Testing Tools.
  • Some payment method IDs differ in the sandbox, for example CBU/CVU (Momentum) is 18 instead of 63. See Payment Methods.
  • The simulators only produce successful deposits. You cannot simulate a bounced payout, so test your 70 and 72 handling with recorded payloads.

Go-live checklist

  • The wallet is funded before orders are created, or post-funded off-ramp is enabled and your system funds orders in 32 before orderExpireTimestamp.
  • Beneficiaries are registered once, stored by userPaymentId, and only used when status is 1.
  • Every create request sends a unique clientId, and timeouts are resolved by looking up the clientId before retrying.
  • The ORDER_WEBHOOK endpoint verifies the signature, returns {"callbackStatus":"SUCCESS"} with HTTP 2xx, and deduplicates on orderId + orderStatus.
  • 70 and 72 (including 72 after 60) release or reverse the payout on your side, and a replacement is only created after the original is final.
  • Orders stuck in 40 or 50 are flagged and escalated with the orderId.
  • For USD and EUR, the CPN account is activated and CPN beneficiaries include the required compliance fields.
See the full Go-live checklist.