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

# Pay out stablecoins via Pix

> Pay out USDC or USDT to a Pix key in Brazil: register the destination, create an off-ramp to BRL, and follow it with webhooks until it settles.

To pay out stablecoins via Pix with Lumx, you register the recipient's Pix key as a destination, then call `POST /transactions/off-ramp` with USDC or USDT as the source. Lumx debits the customer's wallet, converts to BRL and sends the Pix payment to the key. The customer's BRL account is in its own name, so the recipient sees the customer as the payer. You follow each payout with `offramp.*` webhooks until `offramp.success`.

Pix runs 24/7/365 with no cut-off, so a payout does not wait for a banking day.

## What you need

* An API key from the [Dashboard](https://dashboard.lumx.io). The examples use the sandbox base URL.
* An `APPROVED` [customer](https://docs.lumx.io/concepts/customers.md). Wallets are enabled only after approval.
* USDC or USDT in the customer's [wallet](https://docs.lumx.io/concepts/wallets.md). In sandbox, run an on-ramp first: it completes on its own and credits the stablecoin to the wallet, and you can pay out from that balance.

Stablecoin and network pairs that convert to BRL:

| **Network** | **Stablecoins** |
| :- | :- |
| `ETHEREUM` | USDC, USDT |
| `POLYGON` | USDC, USDT |
| `BASE` | USDC |
| `TRON` | USDT |

## 1. Create the customer

The customer is the payer. For your own treasury it is your business, and for a platform it is each end user. Send a `POST` request to `/customers`.

```bash Request theme={null}
curl -X POST https://api-sandbox.lumx.io/customers \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "type": "BUSINESS",
    "legalName": "Lumx S.A",
    "taxId": "42.887.120/0001-00",
    "incorporationDate": "2020-01-01",
    "country": "BRA",
    "email": "hello@lumx.io"
  }'
```

The customer starts in `NOT_STARTED` and has to reach `APPROVED` through KYC or KYB. Subscribe to `customer.approved` to know when it does. [Create a customer](https://docs.lumx.io/guides/create-a-customer.md) covers the full flow.

## 2. Register the Pix destination

A destination holds the recipient's Pix key and the holder's details. Send a `POST` request to `/destinations`.

```bash Request theme={null}
curl -X POST https://api-sandbox.lumx.io/destinations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
    "name": "Supplier - Sao Paulo",
    "rail": "PIX",
    "currency": "BRL",
    "identifier": {
      "keyType": "CNPJ",
      "keyValue": "42.887.120/0001-00"
    },
    "holder": {
      "type": "BUSINESS",
      "relationship": "SUPPLIER",
      "legalName": "Lumx S.A",
      "taxId": "42.887.120/0001-00",
      "incorporationDate": "2020-01-01",
      "email": "hello@lumx.io",
      "address": {
        "line1": "Rua Voluntarios da Patria 89",
        "city": "Rio de Janeiro",
        "state": "RJ",
        "postalCode": "22270-000",
        "country": "BRA"
      }
    }
  }'
```

The response returns the destination `id`, which you use as `destinationId` on the payout.

| **Field** | **Description** |
| :- | :- |
| `identifier.keyType` | `CPF`, `CNPJ`, `EMAIL`, `PHONE` or `RANDOM` |
| `identifier.keyValue` | The Pix key. Phone keys use E.164 format, for example `+5511987654321` |
| `holder.relationship` | How the holder relates to the customer. `SELF` for the customer's own account, or a third-party value such as `SUPPLIER` or `EMPLOYEE` |
| `holder.taxId` | Holder's CPF or CNPJ |
| `holder.address` | Holder's address |

Lumx verifies each holder. Check `holder.verification.status` on the response or subscribe to `destinations.approved` and `destinations.final_rejection`. For `SELF` destinations, `holder.type` and `holder.taxId` must match the customer. Relationship values are listed in [Destinations](https://docs.lumx.io/concepts/destinations.md#holder-relationships). An off-ramp to a destination that is not `APPROVED` fails with `INVALID_PAYMENT_DESTINATION`.

## 3. Quote the payout (optional)

Without a quote, the off-ramp converts at a floating rate. To show the recipient amount up front, request a locked quote with `POST /exchange-rates` (`type` `LOCKED`, `rail` `PIX`, `sourceCurrency` `USDC`, `targetCurrency` `BRL`) and pass its `id` as `exchangeRateId` in step 4. The [locked quote guide](https://docs.lumx.io/guides/locked-fx-quote-usdc-to-brl.md) has the request, the lock durations and what happens on expiry.

## 4. Create the off-ramp

Send a `POST` request to `/transactions/off-ramp`.

```bash Request theme={null}
curl -X POST https://api-sandbox.lumx.io/transactions/off-ramp \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "destinationId": "e80d3137-eddd-4791-b9b8-6e36b289f284",
    "sourceCurrency": "USDC",
    "sourceAmount": "1000.00",
    "purpose": "TRADE_TRANSACTIONS",
    "blockchain": "POLYGON",
    "metadata": { "invoiceId": "INV-2026-1042" }
  }'
```

```json Response (202) theme={null}
{
  "id": "123e4567-e89b-12d3-a456-426614174003",
  "customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
  "type": "OFF_RAMP",
  "request": {
    "destinationId": "e80d3137-eddd-4791-b9b8-6e36b289f284",
    "sourceCurrency": "USDC",
    "sourceAmount": "1000.00",
    "purpose": "TRADE_TRANSACTIONS"
  },
  "state": {
    "status": "TRANSFERRING_STABLECOIN"
  },
  "metadata": { "invoiceId": "INV-2026-1042" },
  "createdAt": "2026-11-02T15:30:00Z",
  "updatedAt": "2026-11-02T15:30:05Z"
}
```

| **Field** | **Required** | **Description** |
| :- | :- | :- |
| `destinationId` | Yes | The Pix destination |
| `sourceCurrency` | Yes | `USDC` or `USDT` |
| `sourceAmount` | Yes | Amount of stablecoin to sell |
| `purpose` | Yes | End use of the funds. `PERSONAL_ACCOUNT` works only with `SELF` destinations. See [Purpose codes](https://docs.lumx.io/additional-information/purpose-codes.md) |
| `blockchain` | No | Network to debit. Falls back to your project's default |
| `partnerFeeId` | No | [Partner fee](https://docs.lumx.io/concepts/partner-fees.md) for this payout |
| `memo` | No | Text sent in the message field of the Pix |
| `metadata` | No | Any JSON object. Returned on the transaction and in webhooks |

Lumx does not deduct a fee from the amount that reaches the recipient. Only the recipient's bank can charge them.

## 5. Track it until it settles

An off-ramp moves through these statuses:

`TRANSFERRING_STABLECOIN` → `TRADING` → `TRANSFERRING_FIAT` → `SUCCESS`

If it cannot complete, it ends in `FAILED`. Each status change sends a [webhook](https://docs.lumx.io/developer/webhooks.md) to the endpoint you set up under **Developers > Webhooks** in the Dashboard.

| **Event** | **Meaning** |
| :- | :- |
| `offramp.transferring_stablecoin` | The stablecoin is leaving the customer's wallet |
| `offramp.trading` | The conversion is running |
| `offramp.transferring_fiat` | The Pix payment is on its way |
| `offramp.success` | The payout is complete. The receipt has the amounts, rate, fees and `transactionHash` |
| `offramp.failed` | The payout failed |

```json offramp.success (excerpt) theme={null}
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "eventType": "offramp.success",
  "data": {
    "id": "123e4567-e89b-12d3-a456-426614174003",
    "type": "OFF_RAMP",
    "state": { "status": "SUCCESS" }
  }
}
```

Verify the signature on every request using the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers, and deduplicate on `webhook-id`. If your endpoint does not return a `2xx`, Lumx retries with exponential backoff. If a webhook is missed, `GET /transactions/{id}` returns the current state.

## Timing and limits

* **Pix settlement.** Instant, 24/7/365, with no cut-off. These are processing targets, not guarantees. See [Payment rail cut-off times](https://docs.lumx.io/additional-information/sla-and-cutoffs.md).
* **Holds.** A transaction flagged by compliance monitoring is held for an [RFI](https://docs.lumx.io/compliance/rfi.md). Large transactions can get an extra risk review, which typically adds up to two business hours.
* **Completion.** Lumx treats the off-ramp as complete once the funds leave its banking partner. The recipient's bank can still hold them for its own review.
* **Limits.** Off-ramps count only against `offramp` limits, per transaction, per day and per month, counted in the USD equivalent of the payout. The minimum per transaction is the equivalent of USD 40 today, and it may change. Read the remaining headroom with `GET /customers/{id}?includeTransactionLimits=true`. See [Transaction limits](https://docs.lumx.io/compliance/transaction-limits.md).

## Common errors

Match on `code`, not on `message`.

| **Status** | **Code** | **Cause and fix** |
| :- | :- | :- |
| `400` | `VALIDATION_ERROR` | A field is invalid. Details are in `validationErrors` |
| `400` | `PERSONAL_ACCOUNT_RELATIONSHIP_REQUIRED` | `PERSONAL_ACCOUNT` was used with a destination that is not `SELF`. Pick the purpose that matches the payment |
| `400` | `INVALID_TAX_ID` | The bank rejected the tax ID. Check `holder.taxId` |
| `400` | `INVALID_PAYMENT_DESTINATION` | The destination is not `APPROVED`, or its rail or currency does not match the payout |
| `403` | `KYC_NOT_APPROVED` | The customer is not approved yet |
| `403` | `HOLDER_TAX_ID_MISMATCH` | A `SELF` destination has a tax ID that differs from the customer's |
| `404` | `DESTINATION_NOT_FOUND` | The `destinationId` does not exist |
| `409` | `IDEMPOTENCY_KEY_CONFLICT` | The same `Idempotency-Key` was sent with a different body |
| `422` | `INSUFFICIENT_BALANCE` | The wallet holds less than `sourceAmount` |
| `422` | `MINIMUM_AMOUNT_NOT_MET` | The payout is below the minimum, the equivalent of USD 40 today (it may change) |
| `422` | `TRANSACTION_LIMIT_EXCEEDED` | The amount passes the customer's single, daily or monthly limit |
| `422` | `EXCHANGE_RATE_EXPIRED` | A locked quote expired before the call. Request a new one |

See [Errors](https://docs.lumx.io/developer/errors.md) for the full catalog.

## Related resources

> [card] [Locked FX quote guide](https://docs.lumx.io/guides/locked-fx-quote-usdc-to-brl.md)
>
> Fix the USDC to BRL rate before you pay out.

> [card] [Destinations](https://docs.lumx.io/concepts/destinations.md)
>
> Holder relationships and payout targets.

> [card] [Webhooks](https://docs.lumx.io/developer/webhooks.md)
>
> Events, signatures and retries.

> [card] [Coverage](https://docs.lumx.io/get-started/coverage.md)
>
> Supported rails, networks and stablecoins.


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