Skip to main content

Flow of Funds

Refresher on how instant on-ramping works with Superbank’s API.

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 (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

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:
BranchPicked whenWho pays the end-userFUNDS_SENT required
prefunded: trueYour prefunded balance ≥ amountYou — Superbank fronts the stablecoin from your prefunded wallet the moment you call FUNDS_SENT.Yes
prefunded: falsePrefunded balance insufficientSuperbank — 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.
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).

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

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 for retention, scope, error codes, and dispatcher pattern.
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.
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.
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.

Request — USD via ACH (US)

cURL
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": "REMITTANCES",
    "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.
cURL
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": "REMITTANCES",
    "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"
        }
      }
    }
  }'
destination.is_third_party — pick what’s true, not what’s convenient. Set true when the destination wallet belongs to the end-user (not to your company). Set false only when the destination is your own company / registered account.

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 for the full schema.
{
  "id": "39760060-846e-4d5a-8583-7ee62553f79b",
  "type": "FIAT_TO_STABLECOIN",
  "payment_reason": "REMITTANCES",
  "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"
}
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.
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.
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.

Response — prefunded: false

Identical shape — only prefunded flips to false. The deposit slip fields are the same; the lifecycle differs from Step 4 onwards.
{
  "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"
  },
  "...": "..."
}
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.

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

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

Request

cURL
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

{
  "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: trueSETTLEMENT_COMPLETED confirms the end-user has their stablecoin. A second event with REQUEST_COMPLETED lands later when fiat reconciliation closes.
  • prefunded: falseREQUEST_COMPLETED is the single terminal event. There is no intermediate FUNDS_SENT because Superbank drives the lifecycle without dev input.
{
  "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.

Polling (fallback)

If you can’t accept webhooks, poll GET /v0/settlement-requests/:id until status === 'REQUEST_COMPLETED'. Recommended interval: 5 seconds or longer.
cURL
curl 'https://api-sandbox.superbank.co/v0/settlement-requests?external_id=txn_abc123' \
  --header 'X-Api-Key: YOUR_API_KEY'

Failure modes

StatusWhen it happensWhat to do
REQUEST_EXPIREDEnd-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_MISMATCHFiat 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_FAILEDFiat 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_FAILEDStablecoin 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

Webhooks

Wire up a webhook handler to detect SETTLEMENT_COMPLETED / REQUEST_COMPLETED without polling — envelope, headers, signature verification, and the full event list.

Stablecoin → Stablecoin 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.