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

# Real Time Off-Ramping

<Card title="Flow of Funds" icon="link" href="/get-started/flow-of-funds">
  Refresher on how instant off-ramping works with Superbank's API.
</Card>

## Pre-requisites

Before you begin, ensure you have:

1. A Superbank Developer account with API access
2. Your API key
3. A wallet you control that holds the source stablecoin on the chain you'll use
4. A destination bank account that supports an **instant payment rail**

[Click here to learn how](/platform/instructions/get-api-access)

<Warning>
  **The destination must support a real-time rail.** Off-ramping is destination-amount-driven and
  the locked quote has a short validity window — slow rails (`ACH`, `DOMESTIC_WIRE`) can be used but the
  end-to-end UX is no longer "instant". Use one of these for true real-time settlement:

  * **US (USD)** — `RTP`
  * **Canada (CAD)** — `LOCAL` (Interac e-Transfer)
  * **Eurozone (EUR)** — `SEPA_INSTANT`
  * **Mexico (MXN)** — `SPEI`
  * **Nigeria (NGN)** — `NIBSS`
</Warning>

## How off-ramping differs from on-ramping

Off-ramping (stablecoin → fiat) uses a **destination-amount-driven** flow with a locked quote.
Two practical implications worth knowing up front:

* **You specify what the end-user *receives*** (e.g. `destination.amount: 100` USD, or
  `1000` CAD). Superbank quotes the source crypto amount needed to deliver that — fees and
  FX baked in.
* **The deposit wallet is per-payment and short-lived.** Each settlement returns a unique
  `payment_instructions.wallet_address` valid for **15 minutes**. After that the quote
  expires and you'll need a new settlement request.

## Off-Ramping

### Step 1: Receive an off-ramping request

Your end-user requests to off-ramp through your UI or API. They specify the destination bank
account and how much fiat they want to receive.

Persist the destination bank details for the next step.

### Step 2: Create the settlement request

<Tip>
  **Send an `Idempotency-Key` header.** Recommended today, **required
  in a future API version**. Generate a UUID v4 per logical operation
  and include `Idempotency-Key: <uuid>` on the `POST`. Same key
  replayed → we return the original response, no duplicate settlement.
  See the [Idempotency guide](/platform/guides/idempotency) for
  retention, scope, error codes, and dispatcher pattern.
</Tip>

Create a settlement request by sending a `POST` to `/v0/settlement-requests`. The response's
`payment_instructions` block carries the locked quote (`amount`, `exchange_rate`, `fee`) and
the per-payment `wallet_address` you'll send the source stablecoin to.

As with on-ramping, we recommend supplying `external_id` (your internal identifier) and optional
`metadata` — both are echoed back on every read and webhook, and `external_id` is filterable on
the list endpoint.

<Tip>
  **`destination.amount` vs the top-level `amount`.** For STABLECOIN\_TO\_FIAT, set `amount` on the
  destination object — that's what the end-user will *receive*. The top-level `amount` field is
  for STABLECOIN\_TO\_STABLECOIN and FIAT\_TO\_STABLECOIN where source and destination are the same
  currency.
</Tip>

<Tip>
  **`destination.country_code` is required.** ISO 3166-1 alpha-2 (e.g. `US`, `CA`, `DE`). The
  quote engine rejects fiat destinations without it.
</Tip>

#### Example A — USDC on Solana → USD via RTP (same-currency, instant)

Source and destination map 1:1 (USDC ≈ USD), so no FX is performed. The quoted source amount
covers the flat RTP fee plus a small variable component.

##### Request

```bash cURL theme={null}
curl --request POST \
  --url https://api-sandbox.superbank.co/v0/settlement-requests \
  --header 'Content-Type: application/json' \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --data '{
    "type": "STABLECOIN_TO_FIAT",
    "payment_reason": "PERSONAL_TRANSFERS",
    "external_id": "txn_usd_001",
    "metadata": {
      "user_id": "usr_42",
      "source": "mobile_app"
    },
    "source": {
      "currency": "USDC",
      "rail": "SOLANA",
      "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
    },
    "destination": {
      "amount": 100,
      "country_code": "US",
      "currency": "USD",
      "rail": "RTP",
      "is_third_party": false,
      "bank_name": "Chase Bank",
      "account_number": "123456789",
      "routing_number": "021000021",
      "account_type": "CHECKING",
      "beneficiary": {
        "type": "INDIVIDUAL",
        "first_name": "John",
        "last_name": "Doe",
        "email": "john.doe@example.com",
        "address": {
          "country_code": "US",
          "street_line1": "123 Main St",
          "city": "New York",
          "state_region_or_province": "NY",
          "postal_code": "10001"
        }
      }
    }
  }'
```

<Tip>
  **`source.wallet_address`** is *your* sending wallet — the one you'll broadcast the on-chain
  transfer from in Step 3. Superbank records it for KYC/AML correlation; it isn't where the
  funds end up.
</Tip>

##### Response

```json theme={null}
{
  "id": "39760060-846e-4d5a-8583-7ee62553f79b",
  "type": "STABLECOIN_TO_FIAT",
  "payment_reason": "PERSONAL_TRANSFERS",
  "status": "REQUEST_STARTED",
  "amount": "100.00000000",
  "external_id": "txn_usd_001",
  "metadata": { "user_id": "usr_42", "source": "mobile_app" },
  "source": {
    "currency": "USDC",
    "rail": "SOLANA",
    "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
  },
  "destination": {
    "currency": "USD",
    "rail": "RTP",
    "is_third_party": false,
    "bank_name": "Chase Bank",
    "account_number": "123456789",
    "routing_number": "021000021",
    "account_type": "CHECKING",
    "beneficiary": {
      "type": "INDIVIDUAL",
      "first_name": "John",
      "last_name": "Doe",
      "address": { "country_code": "US" }
    }
  },
  "payment_instructions": {
    "amount": "102.04000000",
    "currency": "USDC",
    "rail": "SOLANA",
    "wallet_address": "FRNthe6oqA8fhVMfXMM12rGmFYvejtkmH3bRfYBxWnLB",
    "destination_amount": "100.00000000",
    "destination_currency": "USD",
    "exchange_rate": "1.000000000000000000",
    "fee": { "fixed": 2, "variable": 0.0004 },
    "valid_until": "2026-01-26T15:55:10.074Z"
  },
  "outbound_payment": null,
  "inbound_payment": null,
  "created_at": "2026-01-26T15:40:10.047Z",
  "updated_at": "2026-01-26T15:40:10.047Z"
}
```

The rate, fee, and validity window are all surfaced in `payment_instructions` so you can
render exactly what your end-user will be charged before they confirm.

#### Example B — USDC on Ethereum → CAD via LOCAL (cross-currency, instant)

Cross-currency: source is USDC on Ethereum, destination is CAD via Interac (routed as `LOCAL`).
The quote includes both the CAD-lane fee and the locked FX rate.

##### Request

```bash cURL theme={null}
curl --request POST \
  --url https://api-sandbox.superbank.co/v0/settlement-requests \
  --header 'Content-Type: application/json' \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --data '{
    "type": "STABLECOIN_TO_FIAT",
    "payment_reason": "PERSONAL_TRANSFERS",
    "external_id": "txn_cad_001",
    "source": {
      "currency": "USDC",
      "rail": "ETHEREUM",
      "wallet_address": "0x388C818CA8B9251b393131C08a736A67ccB19297"
    },
    "destination": {
      "amount": 1000,
      "country_code": "CA",
      "currency": "CAD",
      "rail": "LOCAL",
      "is_third_party": false,
      "bank_name": "Royal Bank of Canada",
      "account_number": "1234567",
      "routing_number": "000300003",
      "account_type": "CHECKING",
      "beneficiary": {
        "type": "INDIVIDUAL",
        "first_name": "Ada",
        "last_name": "Lovelace",
        "email": "ada@example.com",
        "address": {
          "country_code": "CA",
          "street_line1": "100 King St W",
          "city": "Toronto",
          "state_region_or_province": "ON",
          "postal_code": "M5X 1E3"
        }
      }
    }
  }'
```

##### Response

```json theme={null}
{
  "id": "8fd117f9-b18b-4197-8099-cf0ecdbfc907",
  "type": "STABLECOIN_TO_FIAT",
  "payment_reason": "PERSONAL_TRANSFERS",
  "status": "REQUEST_STARTED",
  "amount": "1000.00000000",
  "external_id": "txn_cad_001",
  "source": {
    "currency": "USDC",
    "rail": "ETHEREUM",
    "wallet_address": "0x388C818CA8B9251b393131C08a736A67ccB19297"
  },
  "destination": {
    "currency": "CAD",
    "rail": "LOCAL",
    "is_third_party": false,
    "bank_name": "Royal Bank of Canada",
    "account_number": "1234567",
    "routing_number": "000300003",
    "account_type": "CHECKING",
    "beneficiary": {
      "type": "INDIVIDUAL",
      "first_name": "Ada",
      "last_name": "Lovelace",
      "address": { "country_code": "CA" }
    }
  },
  "payment_instructions": {
    "amount": "729.16000000",
    "currency": "USDC",
    "rail": "ETHEREUM",
    "wallet_address": "0x6a21ac1900e94bec9faa79f3a89d7e4ac9b3f218",
    "destination_amount": "1000.00000000",
    "destination_currency": "CAD",
    "exchange_rate": "1.378851220500000000",
    "fee": { "fixed": 1, "variable": 0.004 },
    "valid_until": "2026-01-26T15:55:10.074Z"
  },
  "outbound_payment": null,
  "inbound_payment": null,
  "created_at": "2026-01-26T15:40:10.047Z",
  "updated_at": "2026-01-26T15:40:10.047Z"
}
```

The quote stays locked for the full 15-minute validity
window, so movement in the FX rate during deposit can't affect what the beneficiary receives.

### Step 3: Send the source stablecoin to `payment_instructions.wallet_address`

Broadcast an on-chain transfer of exactly `payment_instructions.amount` of
`payment_instructions.currency` from the wallet you specified in `source.wallet_address` to
`payment_instructions.wallet_address`.

<Warning>
  **Send the exact `payment_instructions.amount` from the wallet in `source.wallet_address`.**
  The quote is locked against that figure. Underpayment will fail to settle. Overpayment may
  settle but the difference isn't recoverable through this flow.
</Warning>

<Warning>
  **The deposit wallet is a placeholder in sandbox.** `payment_instructions.wallet_address`
  comes back as something like `0xExampleAddress1234567890098765432` regardless of the
  source chain. See [Testing in sandbox](#testing-in-sandbox) below for how to drive the
  end-to-end flow without an on-chain transfer.
</Warning>

### Step 4: Wait for `SETTLEMENT_COMPLETED`

Listen for `settlement_request.updated` webhooks. The settlement progresses through:

| Status                 | Meaning                                                                    |
| ---------------------- | -------------------------------------------------------------------------- |
| `REQUEST_STARTED`      | Initial — waiting for your on-chain deposit                                |
| `SETTLEMENT_SENT`      | The deposit was observed; fiat payout is in flight on the destination rail |
| `SETTLEMENT_COMPLETED` | Fiat delivered to the destination bank account                             |

You'll receive a webhook on each transition:

```json theme={null}
{
  "event": "settlement_request.updated",
  "data": {
    "id": "39760060-846e-4d5a-8583-7ee62553f79b",
    "status": "SETTLEMENT_COMPLETED",
    "previous_status": "SETTLEMENT_SENT",
    "amount": "100.00000000",
    "external_id": "txn_usd_001",
    "metadata": { "user_id": "usr_42", "source": "mobile_app" },
    "updated_at": "2026-01-26T15:48:08.670Z"
  },
  "timestamp": "2026-01-26T15:48:08.700Z"
}
```

The full envelope, the `X-Superbank-Event` and `X-Superbank-Signature` headers, and the complete
list of event types are documented in the **[Webhooks guide](/platform/guides/webhook-guide)**.

#### Polling (fallback)

If you can't accept webhooks, poll `GET /v0/settlement-requests/:id` until
`status === 'SETTLEMENT_COMPLETED'`. Recommended interval: **5 seconds or longer**.

### Step 5: Acknowledge with `REQUEST_COMPLETED`

Once `SETTLEMENT_COMPLETED` arrives, close the settlement by calling `REQUEST_COMPLETED` with the
on-chain transaction hash of your Step 3 deposit. This is the audit reference Superbank records
to tie the settlement to its on-chain leg.

#### Request

```bash cURL theme={null}
curl --request PUT \
  --url https://api-sandbox.superbank.co/v0/settlement-requests/39760060-846e-4d5a-8583-7ee62553f79b \
  --header 'Content-Type: application/json' \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --data '{
    "status": "REQUEST_COMPLETED",
    "transaction_hash": "XQHpy5nHmMVFRntktAHQLKB8X4wBGbAHLG9WA6akVfCHKx2WzfnzsrfM3T2fcn8PoaEdD2Ni1ox4NR5Zh1i7zRJ"
  }'
```

#### Response

```json theme={null}
{
  "id": "39760060-846e-4d5a-8583-7ee62553f79b",
  "type": "STABLECOIN_TO_FIAT",
  "status": "REQUEST_COMPLETED",
  "amount": "100.00000000",
  "external_id": "txn_usd_001",
  "completed_at": "2026-01-26T15:48:30.122Z",
  "...": "..."
}
```

## Testing in sandbox

The sandbox lets you exercise the full off-ramp flow end-to-end without ever
moving real crypto on-chain.

<Warning>
  **The deposit wallet is a placeholder in sandbox.** `payment_instructions.wallet_address`
  comes back as something like `0xExampleAddress1234567890098765432` regardless of
  the source chain. Sending real funds to it is impossible — skip the on-chain
  transfer (Step 3 above) and advance the flow with the sandbox transition
  endpoint instead.
</Warning>

### Step 1: Create the settlement request

Same call as [Step 2](#step-2-create-the-settlement-request) above. Save the
`id` from the response — that's `{settlement_id}` below.

### Step 2: Read the outbound payment id

Because Step 3 is a no-op in sandbox, advance the flow by transitioning the
outbound payment directly. Fetch the settlement to grab the payment id:

```bash cURL theme={null}
curl --request GET \
  --url https://api-sandbox.superbank.co/v0/settlement-requests/{settlement_id} \
  --header 'X-Api-Key: YOUR_API_KEY'
```

The relevant field is `outbound_payment.id` — call it `{payment_id}` below.

### Step 3: Transition the payout to `PROCESSING` → `COMPLETED`

Use the sandbox payment-transition endpoint. Each PATCH fires the same
`payment.updated` and `settlement_request.updated` webhooks your production
handler will see.

```bash cURL theme={null}
# Transition 1: PROCESSING
curl --request PATCH \
  --url https://api-sandbox.superbank.co/v0/sandbox/payments/{payment_id}/status \
  --header 'Content-Type: application/json' \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --data '{"status": "PROCESSING"}'

# Transition 2: COMPLETED
curl --request PATCH \
  --url https://api-sandbox.superbank.co/v0/sandbox/payments/{payment_id}/status \
  --header 'Content-Type: application/json' \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --data '{"status": "COMPLETED"}'
```

After the second PATCH the settlement auto-transitions to
`SETTLEMENT_COMPLETED`. See the [Sandbox Testing guide](/platform/guides/sandbox-testing)
for the full set of sandbox endpoints.

### Step 4: Acknowledge with `REQUEST_COMPLETED`

Same call as [Step 5](#step-5-acknowledge-with-request_completed) above. The
`transaction_hash` value isn't validated against any chain in sandbox, so any
non-empty string works:

```bash cURL theme={null}
curl --request PUT \
  --url https://api-sandbox.superbank.co/v0/settlement-requests/{settlement_id} \
  --header 'Content-Type: application/json' \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --data '{
    "status": "REQUEST_COMPLETED",
    "transaction_hash": "sandbox_test_hash_001"
  }'
```

The settlement should now read `status: "REQUEST_COMPLETED"`.

## Common pitfalls

* **Quote expired.** If more than 15 minutes pass between settlement creation and your on-chain
  deposit (`valid_until`), the quote expires. Create a new settlement request — same
  destination, new `payment_instructions` with a fresh rate.

## Next Steps

<Card title="Webhooks" icon="link" href="/platform/guides/webhook-guide">
  Wire up a webhook handler to detect `SETTLEMENT_COMPLETED` — envelope, headers, signature
  verification, and the full event list.
</Card>

<Card title="Real-time On-Ramping" icon="link" href="/platform/guides/real-time-on-ramping">
  The fiat-to-stablecoin counterpart. Same product family, different direction.
</Card>
