> ## 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 On-Ramping (Fiat → Stablecoin)

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

## When to use this guide

Use **`FIAT_TO_STABLECOIN`** when the end-user is depositing **fiat** (e.g.
USD via ACH) and you want to deliver **stablecoin** (e.g. USDC on Solana)
to their wallet. Superbank handles the fiat → stablecoin conversion and
returns ready-to-render deposit instructions (bank name, routing number,
account number, deposit message) on the response.

If the end-user is depositing stablecoin (not fiat), use
[Real Time On-Ramping](/platform/guides/real-time-on-ramping)
(`STABLECOIN_TO_STABLECOIN`) instead.

## Pre-requisites

Before you begin, ensure you have:

1. A Superbank Developer account with API access
2. Your API key
3. The end-user's destination wallet address (chain + address)
4. **Optional but recommended** — a prefunded USDC balance large enough
   to cover typical settlement sizes (see "How `prefunded` is decided"
   below)

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

## How `prefunded` is decided

`FIAT_TO_STABLECOIN` has **two settlement branches** that share the same
request shape but differ in who delivers stablecoin to the end-user.
Superbank picks the branch automatically based on your available
prefunded liquidity at the moment you create the request:

| Branch             | Picked when                       | Who pays the end-user                                                                                      | `FUNDS_SENT` required   |
| ------------------ | --------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------- |
| `prefunded: true`  | Your prefunded balance ≥ `amount` | **You** — Superbank fronts the stablecoin from your prefunded wallet the moment you call `FUNDS_SENT`.     | **Yes**                 |
| `prefunded: false` | Prefunded balance insufficient    | **Superbank** — stablecoin is delivered straight to the end-user wallet once the fiat deposit is received. | **No** — webhook-driven |

You'll see which branch was chosen on the create response (`prefunded:
true|false`). Both branches return identical deposit instructions for the
end-user — the only thing that changes is whether you call `FUNDS_SENT`.

<Tip>
  **`prefunded: true` is the faster experience.** Stablecoin reaches the
  end-user wallet within seconds of your `FUNDS_SENT` call (you've already
  collected the fiat-receipt confirmation on your side). The
  `prefunded: false` branch waits for fiat clearing before the stablecoin
  ships, so it's bounded by the fiat rail's settlement window (e.g. ACH
  same-day for typical US flows).
</Tip>

## Fiat → Stablecoin On-Ramping

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

Your end-user requests to on-ramp through your UI or API. They specify
the destination wallet address (chain + address) and the stablecoin
amount they want to receive.

Persist the destination wallet address — you'll send it on 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 carries:

* The **`prefunded`** flag (`true` or `false`) describing which branch
  Superbank picked.
* A **`payment_instructions`** block carrying the locked quote
  (`exchange_rate`, `fee`) plus the source-side deposit slip — the
  `amount` / `currency` / `rail` the end-user must deposit, the
  receiving bank (`bank_name`, `bic_swift`, `bank_address`), the
  account (`account_number`, `routing_number`), the reference
  (`deposit_message`), and the beneficiary (`account_holder_name`,
  `account_holder_address`). All flat on `payment_instructions` —
  same shape as off-ramp's `payment_instructions`.

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. You don't need to
persist anything from the create response.

<Tip>
  **Top-level `amount` is the destination stablecoin amount.** For
  `FIAT_TO_STABLECOIN`, set the top-level `amount` to the stablecoin
  the end-user should *receive*. The quote engine computes the fiat
  amount they need to deposit (`payment_instructions.amount`) — that's
  the value you display to the end-user, not what you sent in.
</Tip>

<Tip>
  **`source` is required.** Specify the fiat side via
  `source: { currency, rail, country_code }`. No bank account details —
  Superbank generates the receiving deposit instructions for you.
  The `rail` is a Walapay routing token — use `LOCAL` for any country-routed
  local scheme (SEPA, Faster Payments, SPEI, NIBSS, Interac, …); the
  `country_code` selects the scheme. Common pairs:
  `USD` + `ACH` + `US`, `EUR` + `LOCAL` + `DE`,
  `NGN` + `LOCAL` + `NG`, `MXN` + `LOCAL` + `MX`, `CAD` + `LOCAL` + `CA`.
</Tip>

#### Request — USD via ACH (US)

```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": "FIAT_TO_STABLECOIN",
    "payment_reason": "PERSONAL_TRANSFERS",
    "amount": 100,
    "external_id": "txn_abc123",
    "metadata": {
      "user_id": "usr_42",
      "source": "mobile_app"
    },
    "source": {
      "currency": "USD",
      "rail": "ACH",
      "country_code": "US"
    },
    "destination": {
      "currency": "USDC",
      "rail": "SOLANA",
      "is_third_party": true,
      "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "beneficiary": {
        "type": "INDIVIDUAL",
        "first_name": "Jane",
        "last_name": "Doe",
        "address": {
          "country_code": "US"
        }
      }
    }
  }'
```

#### Request — NGN via NIBSS (Nigeria)

Same shape, different fiat corridor. The response carries NGN-side
deposit instructions and applies the live NGN → USDC exchange rate
locked at quote time.

```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": "FIAT_TO_STABLECOIN",
    "payment_reason": "PERSONAL_TRANSFERS",
    "amount": 100,
    "external_id": "txn_ng_001",
    "source": {
      "currency": "NGN",
      "rail": "LOCAL",
      "country_code": "NG"
    },
    "destination": {
      "currency": "USDC",
      "rail": "SOLANA",
      "is_third_party": true,
      "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "beneficiary": {
        "type": "INDIVIDUAL",
        "first_name": "Adaeze",
        "last_name": "Okoro",
        "address": {
          "country_code": "NG"
        }
      }
    }
  }'
```

<Tip>
  **`destination.is_third_party`.** Identifies whether the beneficiary is your
  own business or someone else. Set `false` when the destination account is
  held by your own company, otherwise `true`.
</Tip>

#### Response — `prefunded: true` (NGN → USDC)

Cross-currency example so the locked-quote fields aren't trivial. The
end-user is depositing **150,750.00 NGN** in Lagos via NIBSS and the
end-user wallet receives **100 USDC** on Solana. The 750 NGN spread on
top of the 150,000 NGN mid-market amount is our **50 bps** variable
fee. Some fields are omitted for brevity — see the
[Settlement Request reference](/platform/core-resources/settlement-request)
for the full schema.

```json theme={null}
{
  "id": "39760060-846e-4d5a-8583-7ee62553f79b",
  "type": "FIAT_TO_STABLECOIN",
  "payment_reason": "PERSONAL_TRANSFERS",
  "status": "REQUEST_STARTED",
  "prefunded": true,
  "amount": "100.00000000",
  "external_id": "txn_ng_001",
  "metadata": {
    "user_id": "usr_42",
    "source": "mobile_app"
  },
  "source": {
    "currency": "NGN",
    "rail": "LOCAL"
  },
  "destination": {
    "currency": "USDC",
    "rail": "SOLANA",
    "is_third_party": true,
    "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "beneficiary": {
      "type": "INDIVIDUAL",
      "first_name": "Adaeze",
      "last_name": "Okoro",
      "address": { "country_code": "NG" }
    }
  },
  "payment_instructions": {
    "amount": "150750.00",
    "currency": "NGN",
    "rail": "LOCAL",
    "valid_until": "2026-05-27T15:05:10.074Z",
    "exchange_rate": "0.0006666667",
    "fee": { "fixed": 0, "variable": 0.0050 },
    "destination_currency": "USDC",
    "destination_rail": "SOLANA",
    "bank_name": "Example Nigerian Bank",
    "bic_swift": "EXAMNGLA",
    "bank_address": {
      "street_line1": "12 Marina Road",
      "city": "Lagos",
      "state_region_or_province": "Lagos",
      "postal_code": "101241",
      "country": "NG"
    },
    "account_number": "1000123456",
    "routing_number": "999",
    "deposit_message": "EXAMPLE1234567890",
    "account_holder_name": "Example Beneficiary Name",
    "account_holder_address": {
      "city": "Lagos",
      "country": "NG",
      "postal_code": "101241"
    }
  },
  "outbound_payment": null,
  "inbound_payment": null,
  "created_at": "2026-05-27T15:00:10.047Z",
  "updated_at": "2026-05-27T15:00:10.047Z"
}
```

<Tip>
  **`exchange_rate` is destination per unit of source.** For NGN → USDC
  that's `0.0006666667` (≈ 1,500 NGN per 1 USDC). Same-currency
  corridors (e.g. USD → USDC) return `1.0000000000`. The variable fee
  (`fee.variable`) is applied **on top** of the conversion — it is NOT
  bundled into the rate. So `payment_instructions.amount` =
  top-level `amount` / `exchange_rate` × (1 + `fee.variable`) +
  `fee.fixed`.
</Tip>

<Tip>
  **`fee.variable` is a fraction (bps / 10,000)**. In this example we
  quote **50 bps** = `0.0050` = 0.5 %. Typical fiat corridors we run
  price in the 50 bps range; cross-currency corridors with FX exposure
  may price higher.
</Tip>

<Tip>
  **`fee.fixed` is in source currency and may be `0`.** The shape stays
  the same as off-ramping so you write one parser. When we add a flat
  per-deposit fee on a corridor it surfaces here; today, most corridors
  carry only a variable fee and `fixed` is `0`.
</Tip>

#### Response — `prefunded: false`

Identical shape — only `prefunded` flips to `false`. The deposit slip
fields are the same; the lifecycle differs from Step 4 onwards.

```json theme={null}
{
  "id": "39760060-846e-4d5a-8583-7ee62553f79b",
  "type": "FIAT_TO_STABLECOIN",
  "status": "REQUEST_STARTED",
  "prefunded": false,
  "amount": "100.00000000",
  "source": { "currency": "NGN", "rail": "LOCAL" },
  "payment_instructions": {
    "amount": "150750.00",
    "currency": "NGN",
    "rail": "LOCAL",
    "exchange_rate": "0.0006666667",
    "fee": { "fixed": 0, "variable": 0.0050 },
    "destination_currency": "USDC",
    "destination_rail": "SOLANA",
    "bank_name": "...",
    "account_number": "...",
    "routing_number": "...",
    "deposit_message": "...",
    "...": "same flat shape as prefunded:true"
  },
  "...": "..."
}
```

<Warning>
  **The deposit slip is short-lived.** `payment_instructions.valid_until`
  is **5 minutes** by default. After it expires the settlement
  transitions to `REQUEST_EXPIRED` and a new request must be created —
  the `deposit_message` cannot be reused.
</Warning>

### Step 3: Render the deposit slip and collect the fiat

Read the source-side fields off `payment_instructions` and render them
as a bank-transfer instruction screen. The fields that matter for the
user:

* **`amount` + `currency`** (on `payment_instructions`) — the exact
  amount they must send (includes the conversion fee, in source
  currency).
* **`bank_name` / `account_number` / `routing_number`** — the receiving
  bank. **`bic_swift`** for international wires.
* **`account_holder_name`** — the beneficiary the user enters on their
  bank's transfer form.
* **`deposit_message`** — the memo / reference field. This is how the
  deposit is matched back to your settlement; if it's omitted the
  deposit may be returned.

<Warning>
  **`deposit_message` is mandatory and unique to this settlement.** The
  end-user must include it in the bank-transfer reference / memo
  field. Without it, the deposit cannot be matched and will be returned.
</Warning>

### Step 4 (`prefunded: true` only): Call `FUNDS_SENT`

Once the end-user confirms they've sent the fiat (or you've otherwise
verified the deposit is in flight on your side), call `FUNDS_SENT` to
trigger the instant stablecoin payout from your prefunded wallet to the
end-user's wallet.

<Warning>
  **This step is `prefunded: true` only.** On `prefunded: false`,
  Superbank ships the stablecoin automatically once fiat clears — you
  don't call `FUNDS_SENT` and you'll get a **400** if you try. Branch on
  the `prefunded` flag from Step 2's response.
</Warning>

#### 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": "FUNDS_SENT"
  }'
```

#### Response

```json theme={null}
{
  "id": "39760060-846e-4d5a-8583-7ee62553f79b",
  "type": "FIAT_TO_STABLECOIN",
  "status": "FUNDS_SENT",
  "prefunded": true,
  "amount": "100.00000000",
  "outbound_payment": {
    "id": "2a9d2a2c-d97b-4973-baf1-78e31d37a024",
    "type": "PAYOUT",
    "status": "PROCESSING",
    "amount": "100.00000000",
    "currency": "USDC",
    "created_at": "2026-05-27T15:01:51.589Z"
  },
  "inbound_payment": null,
  "processing_at": "2026-05-27T15:01:51.594Z",
  "completed_at": null,
  "...": "..."
}
```

### Step 5: Settlement completes

What happens next depends on the branch:

* **`prefunded: true`** — the stablecoin payout lands in the end-user
  wallet within seconds (`SETTLEMENT_COMPLETED`). When the fiat deposit
  clears on your side, Superbank automatically reconciles into your
  prefunded wallet and the settlement transitions to
  `REQUEST_COMPLETED`. **You don't need to call `REQUEST_COMPLETED`
  manually** — the webhook fires when reconciliation completes.
* **`prefunded: false`** — Superbank waits for fiat clearing, then ships
  the stablecoin straight to the end-user wallet. The settlement
  transitions `REQUEST_STARTED → FUNDS_SENT → SETTLEMENT_COMPLETED →
  REQUEST_COMPLETED` end-to-end without any further calls from your
  side.

### Detecting Completion

Subscribe a webhook endpoint and listen for `settlement_request.updated`
events. The status you watch for depends on the branch:

* **`prefunded: true`** — `SETTLEMENT_COMPLETED` confirms the end-user
  has their stablecoin. A second event with `REQUEST_COMPLETED` lands
  later when fiat reconciliation closes.
* **`prefunded: false`** — `REQUEST_COMPLETED` is the single terminal
  event. There is no intermediate `FUNDS_SENT` because Superbank drives
  the lifecycle without dev input.

```json theme={null}
{
  "event": "settlement_request.updated",
  "data": {
    "id": "39760060-846e-4d5a-8583-7ee62553f79b",
    "status": "SETTLEMENT_COMPLETED",
    "previous_status": "FUNDS_SENT",
    "amount": "100.00000000",
    "external_id": "txn_abc123",
    "metadata": { "user_id": "usr_42", "source": "mobile_app" },
    "outbound_payment_id": "2a9d2a2c-d97b-4973-baf1-78e31d37a024",
    "updated_at": "2026-05-27T15:02:08.670Z"
  },
  "timestamp": "2026-05-27T15:02: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 === 'REQUEST_COMPLETED'`. Recommended interval: **5
seconds or longer**.

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

### Failure modes

| Status                  | When it happens                                                                                  | What to do                                                                                                                           |
| ----------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `REQUEST_EXPIRED`       | End-user didn't deposit before `payment_instructions.valid_until` passed.                        | Create a new settlement and re-render the deposit slip; the previous `deposit_message` is dead.                                      |
| `PAYIN_AMOUNT_MISMATCH` | Fiat deposit amount differs from `payment_instructions.amount` (rare — usually a fee shortfall). | Inspect `failure_reason` for the observed vs. expected amount. Refund or top-up as your KYC/operations policy dictates.              |
| `PAYIN_FAILED`          | Fiat deposit rejected by the receiving bank (e.g. closed account, KYC bounce).                   | Surface the error to the end-user; create a new settlement once they can retry.                                                      |
| `PAYOUT_FAILED`         | Stablecoin payout to the end-user wallet failed (e.g. invalid address, chain congestion).        | Inspect `failure_reason`. For `prefunded: true`, your prefunded balance is released automatically; create a new settlement to retry. |

## Next Steps

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

<Card title="Stablecoin → Stablecoin on-ramping" icon="link" href="/platform/guides/real-time-on-ramping">
  Same destination shape, but the end-user deposits stablecoin instead
  of fiat. Use this when there's no fiat leg on the source side.
</Card>
