---
updatedAt: 2026-09-30T15:54:20.000Z
---

Fetch the complete documentation index at: https://docs.avvio.xyz/llms.txt. Use this file to discover all available pages before exploring further. Append .md to any documentation page URL to get its markdown version.

# Send your first payout to Mexico

## Set up your sandbox

Create a **test key** in Dashboard → Developers and note your organization id. The key is shown once, so copy it then.

Send the key as `x-api-key` on every request; there is nothing to sign. Production uses the same base URL, and the `avvio_test_` prefix keeps these calls in your sandbox, where no real money moves. The Node tabs need `npm i @avvio/payments`, and the Python tabs need `pip install requests`.

**Each sample mints a new `Idempotency-Key` for its own call.** Reuse a key only to retry that exact request. The same payout under a new key is refused with `409 DUPLICATE_REQUEST_DETECTED` for 15 minutes, and pays twice after that ([Idempotency](/idempotency/)).

```bash title="Setup"
export AVVIO_API_KEY=avvio_test_…
export AVVIO_ORG_ID=cmsx…        # a cuid, not an org_ prefix
export AVVIO_BASE_URL=https://api.avvio.xyz/business/api/v1
```

## Ask what Mexico needs

A `200` proves the key works. Each destination needs different account details, so build your form from this call instead of hardcoding one: the fields follow your routing ([Register recipients](/recipients/)).

Mexico needs one field, `clabeNumber`: 18 digits with a check digit. The response also carries the corridor's `limits`, and `settlement`, which is always `null` today (a payout carries `expectedSettlementAt` instead).

CLI:

```bash
avvio-payments corridors
```

curl:

```bash
curl -s "$AVVIO_BASE_URL/recipients/$AVVIO_ORG_ID/corridors" \
  -H "x-api-key: $AVVIO_API_KEY"
```

Node:

```js
import { PayoutsClient } from '@avvio/payments';

// Reads AVVIO_API_KEY, AVVIO_ORG_ID and AVVIO_BASE_URL from the environment.
const avvio = new PayoutsClient();

const { corridors, capabilities } = await avvio.corridors();
```

Python:

```python
import os
import requests

res = requests.get(
    f"{os.environ['AVVIO_BASE_URL']}/recipients/{os.environ['AVVIO_ORG_ID']}/corridors",
    headers={
        "x-api-key": os.environ["AVVIO_API_KEY"],
    },
)
print(res.status_code, res.json())
```

```json title="Response"
{
  "corridors": [
    {
      "currency": "MXN",
      "fields": [
        { "id": "clabeNumber", "title": "CLABE", "type": "string",
          "pattern": "^[0-9]{18}$", "checksum": "clabe", "required": true }
      ],
      "limits": { "min": "1.00", "max": "5000.00" },
      "settlement": null
    }
  ],
  "capabilities": { "exactOutput": true, "indicativePricing": true }
}
```

## Fund your sandbox balance

Payouts draw on your USD balance. In production you fund it by wire or USDC ([Fund your balance](/funding/)); in the sandbox this call does it, and `amount` defaults to `10000.00`. Skip it once to see the underfunded path, which your integration has to handle.

CLI:

```bash
avvio-payments fund --amount 5000.00
```

curl:

```bash
IDEMPOTENCY_KEY=$(uuidgen)  # new key per call; reuse it only to retry this exact request
curl -s -X POST "$AVVIO_BASE_URL/payments/organizations/$AVVIO_ORG_ID/sandbox/fund" \
  -H "x-api-key: $AVVIO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "content-type: application/json" \
  -d '{
        "amount": "5000.00"
      }'
```

Node:

```js
await avvio.fund('5000.00');   // test keys only
```

Python:

```python
import os
import uuid
import requests

idempotency_key = str(uuid.uuid4())  # new key per call; reuse it only to retry this exact request

res = requests.post(
    f"{os.environ['AVVIO_BASE_URL']}/payments/organizations/{os.environ['AVVIO_ORG_ID']}/sandbox/fund",
    headers={
        "x-api-key": os.environ["AVVIO_API_KEY"],
        "Idempotency-Key": idempotency_key,
    },
    json={
        "amount": "5000.00"
    },
)
print(res.status_code, res.json())
```

```json title="Response"
{ "balance": "5000.00" }
```

## Register the recipient

A recipient is stored once and reused. `type`, `name` and `method` are always required, and `method.recipientDetails` holds the fields the corridor named.

Each tab saves `method.destinationAccountId` as `DESTINATION_ACCOUNT_ID`. You pay that id, not the recipient `id`. `externalId` is your id for the recipient: sending the same one again returns this recipient instead of a second one. `endUserId` names your customer, the person the payout is for, never the recipient. The response below is abridged: the real one also carries `type`, `paymentMethods[]` and more ([Register recipients](/recipients/)).

CLI:

```bash
avvio-payments beneficiary create \
  --name "María González" --email maria@example.com \
  --currency MXN --country MX \
  --end-user customer_42 --external-id cust42_maria \
  --field clabeNumber=012180000080004471 | tee response.json
export DESTINATION_ACCOUNT_ID=$(jq -r .method.destinationAccountId response.json)
```

curl:

```bash
IDEMPOTENCY_KEY=$(uuidgen)  # new key per call; reuse it only to retry this exact request
curl -s -X POST "$AVVIO_BASE_URL/recipients/$AVVIO_ORG_ID" \
  -H "x-api-key: $AVVIO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "content-type: application/json" \
  -d '{
        "type": "individual",
        "name": "María González",
        "country": "MX",
        "externalId": "cust42_maria",
        "endUserId": "customer_42",
        "method": {
          "kind": "fiat",
          "currency": "MXN",
          "recipientDetails": {
            "clabeNumber": "012180000080004471"
          }
        }
      }' | tee response.json
export DESTINATION_ACCOUNT_ID=$(jq -r .method.destinationAccountId response.json)
```

Node:

```js
const beneficiary = await avvio.createBeneficiary({
  name: 'María González',
  email: 'maria@example.com',                // optional contact detail
  country: 'MX',
  currency: 'MXN',
  endUserId: 'customer_42',        // your id for the person sending
  externalId: 'cust42_maria',      // makes a repeat create safe
  details: { clabeNumber: '012180000080004471' },
});
const destinationAccountId = beneficiary.method.destinationAccountId; // you pay this
```

Python:

```python
import os
import uuid
import requests

idempotency_key = str(uuid.uuid4())  # new key per call; reuse it only to retry this exact request

res = requests.post(
    f"{os.environ['AVVIO_BASE_URL']}/recipients/{os.environ['AVVIO_ORG_ID']}",
    headers={
        "x-api-key": os.environ["AVVIO_API_KEY"],
        "Idempotency-Key": idempotency_key,
    },
    json={
        "type": "individual",
        "name": "María González",
        "country": "MX",
        "externalId": "cust42_maria",
        "endUserId": "customer_42",
        "method": {
            "kind": "fiat",
            "currency": "MXN",
            "recipientDetails": {
                "clabeNumber": "012180000080004471"
            }
        }
    },
)
print(res.status_code, res.json())
os.environ["DESTINATION_ACCOUNT_ID"] = res.json()["method"]["destinationAccountId"]  # a later step reads it
```

```json title="Response"
{
  "id": "cmf3k2xa10004q8b7r5t8u1vw",
  "name": "María González",
  "country": "MX",
  "externalId": "cust42_maria",
  "endUserId": "customer_42",
  "method": {
    "kind": "fiat",
    "currency": "MXN",
    "last4": "4471",
    "status": "active",
    "destinationAccountId": "sbx_acct_MXN_4471_ae66cbc5"
  }
}
```

## Show a price

Show the price while someone is still choosing an amount. It needs no recipient and creates nothing; `indicative: true` marks it as an estimate, and the binding price comes back on the payout.

The fee comes out of what you send: `destinationAmount = (sourceAmount − fee) × rate`. Keep `destinationAmount.amount` for the next step.

CLI:

```bash
avvio-payments quote --amount 200.00 --to MXN
```

curl:

```bash
curl -s "$AVVIO_BASE_URL/payments/organizations/$AVVIO_ORG_ID/rates?from=USD&to=MXN&amount=200.00" \
  -H "x-api-key: $AVVIO_API_KEY"
```

Node:

```js
const quote = await avvio.quote({
  amount: '200.00',
  to: 'MXN',
});
// → { sourceAmount, destinationAmount, fee, rate, limits, indicative: true }
```

Python:

```python
import os
import requests

res = requests.get(
    f"{os.environ['AVVIO_BASE_URL']}/payments/organizations/{os.environ['AVVIO_ORG_ID']}/rates",
    headers={
        "x-api-key": os.environ["AVVIO_API_KEY"],
    },
    params={
        "from": "USD",
        "to": "MXN",
        "amount": "200.00"
    },
)
print(res.status_code, res.json())
```

```json title="Response"
{
  "indicative": true,
  "sourceAmount":      { "currency": "USD", "amount": "200.00" },
  "destinationAmount": { "currency": "MXN", "amount": "3384.65" },
  "fee":               { "currency": "USD", "amount": "1.02" },
  "totalDebit":        { "currency": "USD", "amount": "200.00" },
  "rate": "17.010001005126146",
  "limits": { "min": "1.00", "max": "5000.00" }
}
```

## Send the payout

One call prices and sends the payout and answers `200` with it. A `202` means your organization requires approval and no payout exists yet ([Send a payout](/payouts/)).

Pass the amount you showed as `expectDestination`. If the price drifts more than `maxDriftBps` (default 200, or 2%) from it, the call fails with `RATE_DRIFT_EXCEEDED` and nothing is sent.

**A timeout is not a failure.** After a timeout the payout may exist, so retry with the same `Idempotency-Key` to get it back. A new key is refused for 15 minutes (`409 DUPLICATE_REQUEST_DETECTED`) and pays twice after that.

Its `reference` differs from the Quickstart's on purpose. An identical body sent under a new `Idempotency-Key` within 15 minutes is refused with `409 DUPLICATE_REQUEST_DETECTED` and nothing is sent, so change `reference` (or the amount) before you run the same payout twice.

The payout's `rate` is the effective rate after the fee, 3384.65 MXN for 200.00 USD, so it is lower than the quote's 17.01, which applies to the 198.98 left once the 1.02 fee is taken. Keep `payoutId` for the next step.

CLI:

```bash
avvio-payments pay --amount 200.00 \
  --to "$DESTINATION_ACCOUNT_ID" \
  --expect 3384.65 \
  --end-user customer_42 --reference MX-RECIPE-0001
```

curl:

```bash
IDEMPOTENCY_KEY=$(uuidgen)  # new key per call; reuse it only to retry this exact request
curl -s -X POST "$AVVIO_BASE_URL/payments/organizations/$AVVIO_ORG_ID/payouts" \
  -H "x-api-key: $AVVIO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "content-type: application/json" \
  -d '{
        "amount": "200.00",
        "destinationAccountId": "'"$DESTINATION_ACCOUNT_ID"'",
        "amountLeg": "source",
        "expectDestination": "3384.65",
        "maxDriftBps": 200,
        "reference": "MX-RECIPE-0001",
        "endUser": {
          "id": "customer_42"
        }
      }'
```

Node:

```js
const idempotencyKey = crypto.randomUUID(); // persist it with the order so a retry reuses it
const payout = await avvio.payout({
  amount: '200.00',
  destinationAccountId,
  expectDestination: quote.destinationAmount.amount,  // 3384.65
  endUser: { id: 'customer_42' },
  reference: 'MX-RECIPE-0001',
  idempotencyKey,
});
```

Python:

```python
import os
import uuid
import requests

idempotency_key = str(uuid.uuid4())  # new key per call; reuse it only to retry this exact request

res = requests.post(
    f"{os.environ['AVVIO_BASE_URL']}/payments/organizations/{os.environ['AVVIO_ORG_ID']}/payouts",
    headers={
        "x-api-key": os.environ["AVVIO_API_KEY"],
        "Idempotency-Key": idempotency_key,
    },
    json={
        "amount": "200.00",
        "destinationAccountId": os.environ["DESTINATION_ACCOUNT_ID"],
        "amountLeg": "source",
        "expectDestination": "3384.65",
        "maxDriftBps": 200,
        "reference": "MX-RECIPE-0001",
        "endUser": {
            "id": "customer_42"
        }
    },
)
print(res.status_code, res.json())
```

```json title="Response"
{
  "payoutId": "sbx_pay_7c1c0f2e-6d9b-4a3e-9d1a-2f4b8c9e0a11",
  "status": "pending",
  "sourceAmount":      { "currency": "USD", "amount": "200.00" },
  "destinationAmount": { "currency": "MXN", "amount": "3384.65" },
  "destinationAccountId": "sbx_acct_MXN_4471_ae66cbc5",
  "fee": { "currency": "USD", "amount": "1.02" },
  "rate": "16.923250",
  "reference": "MX-RECIPE-0001",
  "endUser": { "id": "customer_42" },
  "createdAt": "2026-08-20T14:03:11.000Z",
  "updatedAt": "2026-08-20T14:03:11.000Z",
  "completedAt": null
}
```

## Watch it settle

You send to `/payouts` and read from `/orders`. Set `payoutId` to the id from the previous step (`payoutId=sbx_pay_…` in a shell). The read is authoritative even when a webhook went missing.

A payout moves `pending → processing → completed`. In the sandbox it completes 10 to 20 seconds after you create it, and a poll may never catch `processing`. A bank can still return a `completed` payout ([Track status & failures](/status/)).

Next, [receive webhooks](/recipes/receive-and-verify-webhooks/) and [reconcile](/recipes/reconcile-with-events/) from `GET /events`.

CLI:

```bash
avvio-payments status $payoutId
```

curl:

```bash
curl -s "$AVVIO_BASE_URL/payments/organizations/$AVVIO_ORG_ID/orders/$payoutId" \
  -H "x-api-key: $AVVIO_API_KEY"
```

Node:

```js
const order = await avvio.getPayout(payout.payoutId);
// pending → processing → completed, and sometimes back to failed
```

Python:

```python
import os
import requests

payoutId = "<payoutId>"

res = requests.get(
    f"{os.environ['AVVIO_BASE_URL']}/payments/organizations/{os.environ['AVVIO_ORG_ID']}/orders/{payoutId}",
    headers={
        "x-api-key": os.environ["AVVIO_API_KEY"],
    },
)
print(res.status_code, res.json())
```

```json title="Response"
{
  "payoutId": "sbx_pay_7c1c0f2e-6d9b-4a3e-9d1a-2f4b8c9e0a11",
  "status": "completed",
  "sourceAmount":      { "currency": "USD", "amount": "200.00" },
  "destinationAmount": { "currency": "MXN", "amount": "3384.65" },
  "destinationAccountId": "sbx_acct_MXN_4471_ae66cbc5",
  "fee": { "currency": "USD", "amount": "1.02" },
  "rate": "16.923250",
  "reference": "MX-RECIPE-0001",
  "endUser": { "id": "customer_42" },
  "createdAt": "2026-08-20T14:03:11.000Z",
  "updatedAt": "2026-08-20T14:03:26.000Z",
  "completedAt": "2026-08-20T14:03:26.000Z"
}
```

## Make it fail on purpose

In the sandbox the account number's last four digits choose the outcome. Register a recipient with each CLABE, as before, and pay it.

- `…0001` fails at the bank with `failureCode: account_invalid`, and the money comes back (`fundsReturned: true`).
- `…0003` completes, then about 30 seconds later (allow a minute) flips to `failed` with `failureCode: returned_by_bank`.

A bank can return a completed payout days later, so a ledger that treats `completed` as final breaks here. Run this before you go live. [Environments & sandbox](/environments/) lists the other suffixes, and [Go live](/going-live/) is the same code with a live key.

```json title="Fails at the bank: account_invalid"
"recipientDetails": { "clabeNumber": "012180000030000001" }
```

```json title="Completes, then is returned by the bank"
"recipientDetails": { "clabeNumber": "012180000000070003" }
```

```json title="Response: GET …/orders/{payoutId}, a minute later"
{ "status": "failed", "failureCode": "returned_by_bank", "fundsReturned": true }
```
