Skip to main content
This recipe converts local currency (for example MXN, ARS, COP or BRL) into a stablecoin with a fiat_to_crypto order. You choose how each order is paid: from fiat you already hold in the merchant’s Pending Fund balance, or by a payer who sends a bank transfer for that one order. When the order completes, the crypto is credited to the merchant’s VelaFi wallet. From there you keep it, withdraw it, or have it sent to an external address automatically. For the full field reference, see the On-Ramp guide and Create an On-Ramp Order.

Choose a funding model

If your business can hold a local-currency balance, use the prefunded model. The order is paid the moment it is created, so it cannot expire unpaid, and you know before you commit to the trade whether the funds are there. Pay-per-order depends on the payer sending the exact amount before the window closes, and many pay-per-order orders expire unpaid.

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. The order’s merchantId must be a merchant under your account that has passed verification. See Merchants.
  • An activated currency account. The merchant needs an active account for the fiat currency. See Activate a Merchant Account.
  • Webhooks registered. Register ORDER_WEBHOOK, plus FUNDING_WEBHOOK if you prefund and CRYPTO_WITHDRAW_WEBHOOK if you withdraw. See Webhooks. Your endpoint must return HTTP 2xx with the body {"callbackStatus":"SUCCESS"}, or the delivery counts as failed. See Delivery and Retries.

Build the flow

1

Prefunded only: fund the Pending Fund balance

Get the deposit details for the merchant’s currency account with Get Pending Fund Account (GET /v2/merchant/addfunds), and send the fiat there. Each credited deposit triggers a FUNDING_WEBHOOK event with type DEPOSIT and status 2.Check the current balance with List Merchant Accounts (balance) before you create large orders.If you already collect payments into the merchant’s currency account, those deposits make up this balance. See Collect Local Payments with Virtual Accounts.
2

Optional: lock the rate with a quote

Call Get a Crypto/Fiat Quote with from set to the fiat, to set to the crypto 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, fiat and crypto it was issued for. Request it right before you create the order. Without a quoteId, the order is priced when it is created.
3

Create the order

Call POST /v2/order/fiat_to_crypto. Always send a unique clientId (up to 40 characters) so you can retry safely and find the order later.
Set paymentId to 70 (VelaFi Pending Fund).
VelaFi checks the merchant’s balance for that currency when it creates the order. If the balance does not cover the order amount, the request is rejected with an insufficient-balance error and no order is created. If the balance covers it, the order is paid from the balance right after creation and moves to 60 (Completed), unless it first needs review or documents (10, 11 or 31). The debit appears in Funding Records as a BUY_CRYPTO record.
Response
If the request times out, look the order up by clientId before you retry. See Idempotency and retries.
4

Pay-per-order only: show the payment instructions

Call Get Order with orderType=fiat_to_crypto and show the payer the contents of paymentInfo (for example the CLABE for SPEI) and the exact fiatAmount.When the payer’s transfer is credited, VelaFi applies it to open orders on the same currency account. A deposit that exactly equals an order’s amount is applied to that order first. Otherwise deposits are applied to the oldest open order. An order with a depositAlias is only paid by deposits to that virtual account. An order without one can be paid by any deposit on the currency account, including fiat that is already in the Pending Fund balance.
An order is only paid when the full amount is available. A short payment does not complete the order. Ask payers to send the exact amount, and give each payer a virtual account (depositAlias) so one payer’s transfer cannot pay another payer’s order.
5

Track the order to completion

Wait for ORDER_WEBHOOK events, or poll GET /v2/order/detail?orderType=fiat_to_crypto&clientId={clientId}. The order is done when orderStatus is 60, and has failed when it is 70, 80 or 81. When you poll, an order rejected in review is shown as 80 and one rejected in risk review as 81; the webhook sends both as 70. See Order Statuses. The crypto (cryptoAmount) is then in the merchant’s wallet. Check it with Get Crypto Balance.
6

Optional: send the crypto out of the wallet

You have two options:
  • Automatic withdrawal per order. If VelaFi has enabled automatic on-ramp withdrawal for your account, add network and address to the create request. After the order completes, VelaFi withdraws cryptoAmount to that address and sends ON_RAMP_WITHDRAW_WEBHOOK when the withdrawal succeeds. In production, network must be POL or SPL20. Send both fields or neither. Accounts without the feature are rejected with “You are currently not a user of the onRamp automatic withdrawal service.” See On-Ramp Withdrawal Event.
  • Withdraw it yourself. Add the destination to your withdrawal whitelist, then call Create a Crypto Withdrawal (POST /v2/assets/withdraw) with your own unique clientId. Use Get Withdrawal Quota for valid chainType values and minimums.
Both kinds of withdrawal also trigger CRYPTO_WITHDRAW_WEBHOOK when they reach a final status. For an automatic withdrawal, its clientId is the on-ramp orderId.

Webhooks to handle

Events can arrive more than once and out of order, and intermediate statuses can be skipped. Deduplicate, and read the order with GET /v2/order/detail when an event does not fit the state you hold. See Delivery and Retries.

Handle failures

The payment window

A pay-per-order order waits in 40 only for a limited time. VelaFi sets the window for your account. It is not returned in the order detail, so ask your VelaFi contact for the value and show the payer a deadline based on createTime. When the window passes without a full payment, the order is cancelled. It is reported as orderStatus 70 in order detail and in ORDER_WEBHOOK. A cancelled order cannot be reopened. If the payer pays after the order was cancelled, the transfer is still credited to the merchant’s Pending Fund balance (you receive a FUNDING_WEBHOOK DEPOSIT). It is not applied to the cancelled order. Create a new order for that amount, which is then paid from the balance, or pay the money out with Claim Pending Funds.

Reconcile

  • Orders: Get Order by orderId or clientId is the source of truth for each order: orderStatus, fiatAmount, fiatFee, cryptoAmount, orderPrice, failCode and failReason. Use List Orders with a time range for daily totals. It also lists orders not created through the API.
  • Fiat balance: List Funding Records shows each DEPOSIT and BUY_CRYPTO, for both funding models. List Fiat Flows gives the line-by-line ledger with running balances.
  • Crypto out: Get Crypto Withdrawal History by clientId, until status is 6 (successful), 7 (failed) or 9 (cancelled).
Run a scheduled job that re-reads every order still open on your side. 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.
  • Fund a test merchant’s balance, or pay a pay-per-order order, with the fiat deposit simulator (depositAlias targets a virtual account). See Sandbox Testing Tools.
  • In the sandbox, a quoteId lasts 300 seconds, and automatic withdrawal accepts POL or TSPL20 as network.
  • The simulators only produce successful deposits. You cannot simulate a returned payment. Test the expiry path by creating a pay-per-order order and not paying it.

Go-live checklist

  • Every create request sends a unique clientId, and timeouts are resolved by looking up the clientId before retrying.
  • Prefunded flows check the balance and handle the insufficient-balance rejection without retrying the same request in a loop.
  • Pay-per-order flows show paymentInfo, the exact amount and a deadline, and create a new order after 70.
  • Each payer has a virtual account (depositAlias) if you run pay-per-order for several payers on one currency account.
  • The ORDER_WEBHOOK endpoint verifies the signature, returns {"callbackStatus":"SUCCESS"} with HTTP 2xx, and deduplicates on orderId + orderStatus.
  • A scheduled job reconciles open orders, funding records and withdrawals against the API.
  • Withdrawal destinations are whitelisted, or automatic withdrawal is enabled and tested with network and address.
See the full Go-live checklist.