> ## Documentation Index
> Fetch the complete documentation index at: https://docs.velafi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Convert Local Currency to Stablecoins

> Buy USDT or USDC with local currency through a fiat_to_crypto order, paid from your Pending Fund balance or by a payer for each order, and deliver the crypto to your wallet or an external address.

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](https://docs.velafi.com/docs/guides/on-ramp.md) and [Create an On-Ramp Order](https://docs.velafi.com/api-reference/order/create-a-fiat-to-crypto-order.md).

## Choose a funding model

| | Prefunded (recommended) | Pay-per-order |
| - | - | - |
| How the order is paid | From the merchant's Pending Fund balance for that currency | The payer sends a bank transfer to the payment details returned for the order |
| `paymentId` | `70` (VelaFi Pending Fund) | The currency account's channel, for example `105` (SPEI, Finco Pay) |
| Balance check | At creation: the request is rejected if the balance does not cover the order | None at creation |
| Typical result | Completes within seconds of creation | Completes when the payment arrives. If no payment arrives within the payment window, the order is cancelled |
| What you handle | Keeping the balance topped up | Showing payment instructions, payer delays, expired orders |

> [!TIP]
>
> 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

```mermaid theme={null}
flowchart TD
    A[Optional: get a quote with quoteId] --> B{Funding model}
    B -->|Prefunded| C["POST /v2/order/fiat_to_crypto<br/>paymentId 70"]
    C --> D[Order paid from the Pending Fund balance]
    B -->|Pay-per-order| E["POST /v2/order/fiat_to_crypto<br/>channel paymentId"]
    E --> F[Show paymentInfo to the payer]
    F --> G{Paid within the window?}
    G -->|Yes| D2[Deposit credited and matched to the order]
    G -->|No| X["Order cancelled: orderStatus 70"]
    D --> H["Order completed: orderStatus 60<br/>crypto in the merchant wallet"]
    D2 --> H
    H --> I{Deliver the crypto}
    I -->|Keep| J[Hold in the merchant wallet]
    I -->|Automatic withdrawal| K[Sent to the order's address<br/>ON_RAMP_WITHDRAW_WEBHOOK]
    I -->|Manual withdrawal| L["POST /v2/assets/withdraw<br/>CRYPTO_WITHDRAW_WEBHOOK"]
```

## 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](https://docs.velafi.com/api-reference/authentication.md).
* **A verified merchant.** The order's `merchantId` must be a merchant under your account that has passed verification. See [Merchants](https://docs.velafi.com/docs/merchants.md).
* **An activated currency account.** The merchant needs an active account for the fiat currency. See [Activate a Merchant Account](https://docs.velafi.com/docs/guides/activating-merchant-account.md).
* **Webhooks registered.** Register `ORDER_WEBHOOK`, plus `FUNDING_WEBHOOK` if you prefund and `CRYPTO_WITHDRAW_WEBHOOK` if you withdraw. See [Webhooks](https://docs.velafi.com/docs/guides/webhooks.md). Your endpoint must return HTTP `2xx` with the body `{"callbackStatus":"SUCCESS"}`, or the delivery counts as failed. See [Delivery and Retries](https://docs.velafi.com/api-reference/webhooks/webhook-setup.md#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](https://docs.velafi.com/api-reference/merchant/get-pending-fund-account.md) (`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](https://docs.velafi.com/api-reference/merchant/get-a-list-of-merchant-accounts.md) (`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](https://docs.velafi.com/docs/use-cases/collect-with-virtual-accounts.md).

**2. Optional: lock the rate with a quote**

Call [Get a Crypto/Fiat Quote](https://docs.velafi.com/api-reference/quote/get-user-quote-for-crypto-fiat.md) with `from` set to the fiat, `to` set to the crypto and `createQuoteId=true`. The `price` is fiat per 1 unit of crypto.

```bash theme={null}
curl "https://api.velafi.com/v2/user/crypto-quote?country=Mexico&from=MXN&to=USDT&createQuoteId=true" \
  -H "X-BH-TOKEN: YOUR_TOKEN"
```

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.

> [tab] Prefunded
>
> Set `paymentId` to `70` (VelaFi Pending Fund).
>
> ```bash theme={null}
> curl -X POST https://api.velafi.com/v2/order/fiat_to_crypto \
>   -H "X-BH-TOKEN: YOUR_TOKEN" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "merchantId": 100001,
>     "clientId": "onramp-0001",
>     "country": "Mexico",
>     "fiat": "MXN",
>     "crypto": "USDT",
>     "fiatAmount": 1000,
>     "paymentId": 70,
>     "quoteId": "YOUR_QUOTE_ID"
>   }'
> ```
>
> 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](https://docs.velafi.com/api-reference/merchant/retrieve-funding-records.md) as a `BUY_CRYPTO` record.

> [tab] Pay-per-order
>
> Set `paymentId` to the channel of the merchant's currency account, for example `105` for SPEI (Finco Pay). See [Payment Methods](https://docs.velafi.com/docs/payment-methods.md). If you give each payer their own virtual account, also send that account's `depositAlias`.
>
> ```bash theme={null}
> curl -X POST https://api.velafi.com/v2/order/fiat_to_crypto \
>   -H "X-BH-TOKEN: YOUR_TOKEN" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "merchantId": 100001,
>     "clientId": "onramp-0002",
>     "country": "Mexico",
>     "fiat": "MXN",
>     "crypto": "USDT",
>     "fiatAmount": 1000,
>     "paymentId": 105,
>     "depositAlias": "PAYER0001"
>   }'
> ```
>
> The order starts in `40` (Pending payment), or in `10`, `11` or `31` when review or documents are needed.

```json Response theme={null}
{ "code": 200, "msg": "SUCCESS", "data": { "orderId": 1123123123 } }
```

If the request times out, look the order up by `clientId` before you retry. See [Idempotency and retries](https://docs.velafi.com/api-reference/order/create-a-fiat-to-crypto-order.md#idempotency-and-retries).

**4. Pay-per-order only: show the payment instructions**

Call [Get Order](https://docs.velafi.com/api-reference/order/retrieve-a-specific-order.md) 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.

> [!WARNING]
>
> 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](https://docs.velafi.com/docs/order-statuses.md). The crypto (`cryptoAmount`) is then in the merchant's wallet. Check it with [Get Crypto Balance](https://docs.velafi.com/api-reference/merchant/get-crypto-assets-balance.md).

**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](https://docs.velafi.com/api-reference/webhooks/on-ramp-withdraw-message-push.md).
* **Withdraw it yourself.** Add the destination to your [withdrawal whitelist](https://docs.velafi.com/api-reference/crypto-wallet/add-whitelist-address.md), then call [Create a Crypto Withdrawal](https://docs.velafi.com/api-reference/crypto-wallet/create-crypto-withdrawal.md) (`POST /v2/assets/withdraw`) with your own unique `clientId`. Use [Get Withdrawal Quota](https://docs.velafi.com/api-reference/crypto-wallet/get-withdrawal-quota.md) 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

| Event | When | Key on | What to do |
| - | - | - | - |
| `ORDER_WEBHOOK` (orders created through the API only) | The order enters `10`, `11`, `12`, `31`, `40`, `50`, `60`, `70` or `72` | `orderId` + `orderStatus` (`orderType` is `BUY`) | Mark the order complete on `60`, failed on `70`. Orders rejected in review are sent as `70`. |
| `FUNDING_WEBHOOK` | A Pending Fund record completes: `DEPOSIT` (top-up or payer transfer), `BUY_CRYPTO` (fiat applied to an order) | `txId` + `status` | Track the fiat balance. |
| `ON_RAMP_WITHDRAW_WEBHOOK` | An automatic withdrawal succeeded | `orderId` | Record `txHash`. Failed withdrawals do not send this event. |
| `CRYPTO_WITHDRAW_WEBHOOK` | A withdrawal reached `SUCCESS`, `FAILED` or `CANCELLED` | `withdrawId` + `status` | Mark the withdrawal final. |

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](https://docs.velafi.com/api-reference/webhooks/webhook-setup.md#delivery-and-retries).

## Handle failures

| What you see | Likely cause | What to do |
| - | - | - |
| Create rejected with an insufficient-balance error (prefunded) | The Pending Fund balance does not cover the order | Top up the balance or lower the amount, then create the order again with a new `clientId` and a new quote. |
| Create rejected as a repeated submission | The `clientId` was already used, possibly by your own earlier request that timed out | Look the order up by `clientId`. Do not create it again. |
| Create rejected after you passed a `quoteId` | The quote expired, was already used, or was issued for another currency pair | Request a new quote. A rejected create can still consume a `quoteId`. |
| `orderStatus` `70` on a pay-per-order order | No full payment arrived within the payment window, or the order was cancelled | Create a new order with a new `clientId` and show the payer the new instructions. |
| `orderStatus` `80` or `81` when you poll (`70` in the webhook) | Rejected in review or risk review | Do not resubmit unchanged. Contact VelaFi if you need the reason. |
| Order stays in `10`, `11` or `31` | Review, risk review, or supporting documents are required | Upload documents with [Upload Documents](https://docs.velafi.com/api-reference/order/upload-invoice-documents-for-a-specific-order.md) when asked. See [Order Statuses](https://docs.velafi.com/docs/order-statuses.md). |
| `ON_RAMP_WITHDRAW_WEBHOOK` never arrives | The automatic withdrawal failed, or was not made | Check the crypto balance and [Get Crypto Withdrawal History](https://docs.velafi.com/api-reference/crypto-wallet/get-crypto-withdrawal-history.md) with `clientId` set to the on-ramp `orderId`. If the crypto is still in the wallet, withdraw it yourself. |

### 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](https://docs.velafi.com/api-reference/merchant/claim-pending-fund.md).

## Reconcile

* **Orders:** [Get Order](https://docs.velafi.com/api-reference/order/retrieve-a-specific-order.md) by `orderId` or `clientId` is the source of truth for each order: `orderStatus`, `fiatAmount`, `fiatFee`, `cryptoAmount`, `orderPrice`, `failCode` and `failReason`. Use [List Orders](https://docs.velafi.com/api-reference/order/retrieve-a-list-of-orders.md) with a time range for daily totals. It also lists orders not created through the API.
* **Fiat balance:** [List Funding Records](https://docs.velafi.com/api-reference/merchant/retrieve-funding-records.md) shows each `DEPOSIT` and `BUY_CRYPTO`, for both funding models. [List Fiat Flows](https://docs.velafi.com/api-reference/merchant/get-a-list-merchant-fiat-flow.md) gives the line-by-line ledger with running balances.
* **Crypto out:** [Get Crypto Withdrawal History](https://docs.velafi.com/api-reference/crypto-wallet/get-crypto-withdrawal-history.md) 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](https://docs.velafi.com/docs/sandbox.md#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](https://docs.velafi.com/docs/use-cases/go-live-checklist.md).


## Related topics

- [Core Use Cases](https://docs.velafi.com/docs/use-cases/overview.md)
- [Agent Skills](https://docs.velafi.com/docs/developer-tools/agent-skills.md)
- [Pay Out Local Currency Funded by Stablecoins](https://docs.velafi.com/docs/use-cases/payouts-funded-by-stablecoins.md)
- [Introduction](https://docs.velafi.com/docs/introduction.md)
- [VelaFi API Docs](https://docs.velafi.com/index.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.