> ## 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.

# Integrate off-ramps to local bank rails

> A copy-paste prompt that builds Lumx payouts end to end — register a destination with the right holder relationship, quote, off-ramp, settle.

Paste this prompt into your coding agent to pay out from a customer's stablecoin balance to a local bank account. The agent builds a destination for each rail you use, quotes the payout, sends the off-ramp, and tracks it to settlement.

## Before you start

* A Sandbox API key. See [Authentication](/get-started/authentication).
* A customer in `APPROVED` status with a funded wallet. See [Create a customer](/guides/create-a-customer).
* A webhook endpoint registered under **Developers > Webhooks** in the [Dashboard](https://dashboard.lumx.io). See [Webhooks](/developer/webhooks).

## Prompt

```text Prompt theme={null}
You are a senior backend engineer adding Lumx off-ramps — stablecoin out, fiat into a bank account — to an existing server-side application.

Ground truth. Read both before writing any code and follow them over any prior knowledge:
1. https://docs.lumx.io/llms.txt — the documentation index. Every page has a .md twin at its own path.
2. https://lumx-docs-public-prod.s3.us-east-1.amazonaws.com/api-production.yaml — the OpenAPI 3.1 spec. Take field names, required flags and enum values from here; the prose explains the compliance rules the spec cannot.

Work against sandbox at https://api-sandbox.lumx.io with a server-side key sent as "Authorization: Bearer <key>".

1. Generate the destination branches from the spec, do not paraphrase them. POST /destinations is a oneOf with one branch per rail and a different required set on each: PIX takes keyType and keyValue; SPEI takes clabe; ACH and FEDWIRE take accountNumber, routingNumber and type; SWIFT requires bic with either iban or accountNumber; SEPA requires iban and bic; FPS requires sortCode and accountNumber. On SEPA and FPS the holder and bank addresses need only country. Every branch but PIX also requires a bank object — and one of them lists bank as required without defining it, so flag that instead of inventing a shape.
2. Set holder.relationship on every destination from the closed enum — SELF for the customer's own account, or one of the twelve third-party values such as SUPPLIER, EMPLOYEE, CREDITOR. This is a compliance field, not a label: pick the one that is true.
3. Create the destination and keep its id. Confirm with GET /destinations?customerId={id} and handle destinations.under_verification, destinations.approved and destinations.final_rejection. The verification status enums in the spec carry values with no matching event (TEMPORARY_REJECTION on most branches, REJECTED on FPS), so read status from the API and do not drive it from webhooks alone.
4. Quote with POST /exchange-rates when the payer needs a number before committing; type LOCKED returns an id and expiresAt, type FLOATING prices at execution.
5. Pay with POST /transactions/off-ramp using either destinationId + sourceCurrency + sourceAmount + purpose, or destinationId + exchangeRateId + purpose. Send an Idempotency-Key header with a UUID v4 you generated and persisted alongside the payout row before sending. Reuse the stored key on every retry; a retry that mints a new key pays twice.
6. Follow offramp.transferring_stablecoin, offramp.trading, offramp.transferring_fiat, offramp.success and offramp.failed, and confirm with GET /transactions/{id} before you mark the payout settled.

Constraints:
- Every payout debits the wallet of the customer that owns the destination. Do not design a pooled account that pays third parties who are not onboarded — read /compliance/nested-payments and stop if the design requires it.
- PERSONAL_ACCOUNT as purpose is only valid with a SELF destination. Anything else returns 400 PERSONAL_ACCOUNT_RELATIONSHIP_REQUIRED.
- Map errors by code, never by message: INSUFFICIENT_BALANCE, RAIL_NOT_ENABLED, HOLDER_TAX_ID_MISMATCH, DESTINATION_NOT_FOUND, EXCHANGE_RATE_EXPIRED. Treat an unknown code as retryable-unknown and surface it.
- Do not state a rate limit. None is published. Handle 429 TOO_MANY_REQUESTS with exponential backoff.

Deliverables:
- A destination builder with one typed branch per rail and a test per branch.
- A payout function that is idempotent on your own payout id.
- A webhook handler for the five offramp.* events plus the three destinations.* events.
- A note listing every enum value you took from the spec and every rule you took from the prose.
```

## How to use

1. Onboard one sandbox customer to `APPROVED` and fund its wallet before running this — an off-ramp against an empty wallet fails on balance, not on your code.
2. Decide the holder relationship for your real payout flow yourself, with whoever owns compliance. The agent should not guess it.
3. Review the branch tests: a rail you do not use yet is the cheapest thing to delete now and the most expensive to guess later.

## What the prompt builds

| **Endpoint** | **What the agent uses it for** |
| :- | :- |
| `POST /destinations` | Registers the bank account that receives the payout, one branch per rail |
| `GET /destinations` | Reads the destination's verification status |
| `POST /exchange-rates` | Locks a quote when the payer needs a number before committing |
| `POST /transactions/off-ramp` | Sends the payout with an idempotency key |
| `GET /transactions/{id}` | Confirms the final status before marking the payout settled |

Each destination carries a `holder.relationship` that tells Lumx who owns the receiving account. See [Holder relationships](/concepts/destinations#holder-relationships) for the full list.

## Errors to expect

These codes come from the [errors catalog](/developer/errors), for the resources this prompt uses. Match on `code`, not on `message`.

| **Code** | **Status** | **When it happens** |
| :- | :- | :- |
| `INSUFFICIENT_BALANCE` | 422 | Balance is too low for the operation. |
| `RAIL_NOT_ENABLED` | 403 | Rail is not enabled for this project. |
| `HOLDER_TAX_ID_MISMATCH` | 403 | `holderTaxId` differs from the customer tax ID on `SELF` destinations. |
| `DESTINATION_NOT_FOUND` | 404 | Destination does not exist. |
| `EXCHANGE_RATE_EXPIRED` | 422 | Locked rate has expired. |
| `PERSONAL_ACCOUNT_RELATIONSHIP_REQUIRED` | 400 | Personal transactions require a `SELF` bank account. |

## Related resources

<CardGroup cols={2}>
  <Card title="Destinations" href="/concepts/destinations">
    Rails, required fields, and holder relationships.
  </Card>

  <Card title="Nested payments" href="/compliance/nested-payments">
    Whose funds can move, and for whom.
  </Card>

  <Card title="Integrate on-ramps from fiat to stablecoin" href="/prompts/integrate-onramp">
    Take fiat in and deliver stablecoin to the customer's wallet.
  </Card>

  <Card title="Integrate webhooks with signature checks" href="/prompts/integrate-webhooks">
    Receive signed events that survive retries and secret rotation.
  </Card>
</CardGroup>


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