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
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
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
Response
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: You’ll receive a webhook on each transition:
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

Response

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