Skip to main content

Flow of Funds

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

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

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

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’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.
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.
destination.country_code is required. ISO 3166-1 alpha-2 (e.g. US, CA, DE). The quote engine rejects fiat destinations without it.

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
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": "STABLECOIN_TO_FIAT",
    "payment_reason": "REMITTANCES",
    "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"
        }
      }
    }
  }'
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.
Response
{
  "id": "39760060-846e-4d5a-8583-7ee62553f79b",
  "type": "STABLECOIN_TO_FIAT",
  "payment_reason": "REMITTANCES",
  "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
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": "STABLECOIN_TO_FIAT",
    "payment_reason": "REMITTANCES",
    "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
{
  "id": "8fd117f9-b18b-4197-8099-cf0ecdbfc907",
  "type": "STABLECOIN_TO_FIAT",
  "payment_reason": "REMITTANCES",
  "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.
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.
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 below for how to drive the end-to-end flow without an on-chain transfer.

Step 4: Wait for SETTLEMENT_COMPLETED

Listen for settlement_request.updated webhooks. The settlement progresses through:
StatusMeaning
REQUEST_STARTEDInitial — waiting for your on-chain deposit
SETTLEMENT_SENTThe deposit was observed; fiat payout is in flight on the destination rail
SETTLEMENT_COMPLETEDFiat delivered to the destination bank account
You’ll receive a webhook on each transition:
{
  "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.

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

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": "REQUEST_COMPLETED",
    "transaction_hash": "XQHpy5nHmMVFRntktAHQLKB8X4wBGbAHLG9WA6akVfCHKx2WzfnzsrfM3T2fcn8PoaEdD2Ni1ox4NR5Zh1i7zRJ"
  }'

Response

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

Step 1: Create the settlement request

Same call as Step 2 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:
cURL
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 PROCESSINGCOMPLETED

Use the sandbox payment-transition endpoint. Each PATCH fires the same payment.updated and settlement_request.updated webhooks your production handler will see.
cURL
# 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 for the full set of sandbox endpoints.

Step 4: Acknowledge with REQUEST_COMPLETED

Same call as Step 5 above. The transaction_hash value isn’t validated against any chain in sandbox, so any non-empty string works:
cURL
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

Webhooks

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

Real-time On-Ramping

The fiat-to-stablecoin counterpart. Same product family, different direction.