Crypto offramp (stablecoin to fiat)

Use this guide when your business needs to convert stablecoin (USDC or USDT) into fiat and pay an external bank account — without holding a fiat wallet balance first.

Sznd maps a crypto deposit address to each fiat beneficiary (bank account). When you accept an offramp quote, Sznd returns that address together with the amount and funding deadline for that specific transaction. Send the exact stablecoin amount within the window and fiat is delivered to the beneficiary bank account you registered for the customer.

Important: The beneficiary is the fiat payout destination (bank account). It is not the crypto address. Sznd returns the crypto address and transaction payment instructions only after you accept the quote.

Crypto address vs transaction

WhatScopeNotes
Fiat beneficiaryPersistentCreated once per bank account; must be synced before offramp
Crypto deposit addressMapped to the beneficiaryTied to that fiat destination — typically the same address across offramps to the same beneficiary
Amount to sendPer quote acceptAlways use total_payable from the accept response
Funding deadlinePer quote accept15 minutes from accept — show a countdown in your UI
Quote bindingPer transactionYou must accept each quote before sending; deposits are matched to that quote

Do not send crypto without accepting first. Do not reuse an amount or deadline from a previous accept — only the address may repeat for the same beneficiary.


Who this is for

ActorRole
Your businessCalls the Client API: registers customers, creates beneficiaries, creates and accepts quotes, sends stablecoin (or instructs your customer to send).
CustomerThe end user the transaction is for (on_behalf_of). Must complete identity verification before offramp is available.
BeneficiaryThe recipient's bank account where fiat is paid (GBP, EUR, CAD bank transfer, etc.).

End-to-end flow

┌─────────────┐     ┌─────────────┐     ┌──────────────────┐     ┌─────────────────┐│ 1. Register │     │ 2. Create   │     │ 3. Create quote  │     │ 4. Accept quote ││   customer  │────▶│ beneficiary │────▶│ DIRECT_EXCHANGE  │────▶│ (crypto address)│└─────────────┘     │ (fiat bank) │     │ USDC/USDT → fiat │     └────────┬────────┘                    └─────────────┘     └──────────────────┘              │                    ┌─────────────────┐     ┌──────────────────┐   ┌─────────────────┐                    │ 6. Webhook:     │◀────│ 5. Send crypto   │◀──│ Sznd maps crypto│                    │ fiat delivered  │     │ within 15 minutes│   │ address to fiat │                    └─────────────────┘     └──────────────────┘   │ destination     │                                                                    └─────────────────┘

Status lifecycle

CREATEDAWAITING_PAYMENTPAYMENT_CONFIRMEDPROCESSING_PAYOUTCOMPLETED

Poll GET /api/v1/client/quotes/{id} or use transaction webhooks for updates.


Prerequisites

Before your first offramp:

  1. Business API access — HMAC-authenticated Client API key (Authentication).
  2. Customer registeredPOST /api/v1/client/customers or /full.
  3. Customer verification completekyc_state is COMPLETED on GET /api/v1/client/customers/{id}/onboarding-status.
  4. Offramp corridor enabled — Contact Sznd support if GBP, EUR, or CAD bank-transfer offramp is not yet active on your account.
  5. Beneficiary synced — Fiat beneficiary created for the customer; status must allow payout (see Beneficiaries).

Step 1: Register the customer

Register the end customer your business is acting for. Store the returned customer user id — you will pass it as on_behalf_of on quotes and as user_id on beneficiaries.

POST /api/v1/client/customers/full (recommended when you already hold KYC data)

json
{  "email": "customer.a@example.com",  "first_name": "Alex",  "last_name": "Rivera",  "phone": "+447700900123",  "date_of_birth": "1990-04-12",  "country_code": "GB",  "currency": "GBP",  "account_purpose": "personal_or_living_expenses",  "employment_status": "employed",  "source_of_funds": "salary",  "monthly_volume": "5000",  "address": {    "street_1": "10 Example Street",    "city": "London",    "state": "ENG",    "postal_code": "SW1A 1AA",    "country": "GB"  },  "id_card_front": "<base64-or-url>"}

Monitor onboarding until verification is complete:

GET /api/v1/client/customers/{customer_id}/onboarding-status

json
{  "kyc_state": "COMPLETED",  "required_providers": [],  "providers": [    { "provider": "identity", "status": "completed", "required": true }  ]}

Step 2: Create a fiat beneficiary for the customer

Create the bank account that will receive fiat after the stablecoin is converted. Pass the customer's user id in user_id.

POST /api/v1/client/beneficiaries

Example: GBP bank account

json
{  "user_id": "660e8400-e29b-41d4-a716-446655440001",  "account_name": "Alex Rivera",  "email": "customer.a@example.com",  "phone": "+447700900123",  "bank_name": "Example Bank",  "account_number": "12345678",  "sort_code": "123456",  "country_code": "GB",  "currency_code": "GBP"}

Example: EUR bank account (SEPA)

json
{  "user_id": "660e8400-e29b-41d4-a716-446655440001",  "account_name": "Alex Rivera",  "email": "customer.a@example.com",  "phone": "+33601020304",  "bank_name": "Example Bank",  "iban": "FR7630006000011234567890189",  "bic_swift_code": "BNPAFRPP",  "country_code": "FR",  "currency_code": "EUR",  "payment_method": "Sepa"}

Example: CAD bank transfer (EFT)

json
{  "user_id": "660e8400-e29b-41d4-a716-446655440001",  "account_name": "Alex Rivera",  "email": "customer.a@example.com",  "phone": "+14165551234",  "bank_name": "Example Bank",  "account_number": "1234567",  "bank_code": "001",  "routing_number": "12345",  "country_code": "CA",  "currency_code": "CAD",  "payment_method": "EFT",  "holder_type": "individual"}

Store the beneficiary id from the response for the quote.

Field requirements by currency: Beneficiaries API — crypto offramp fiat beneficiaries.


Step 3: Create a DIRECT_EXCHANGE quote

Create a quote with stablecoin as the source and fiat as the target. Set payment_method to CRYPTO and include the network you intend to fund from.

POST /api/v1/client/quotes

json
{  "source_currency": "USDC",  "target_currency": "GBP",  "source_amount": "250.00",  "beneficiary_id": "550e8400-e29b-41d4-a716-446655440000",  "quote_type": "DIRECT_EXCHANGE",  "payment_method": "CRYPTO",  "payment_network": "POLYGON",  "tz": "UTC",  "narration": "Customer A offramp to GBP",  "origin_reference": "OFFRAMP-2026-0042",  "on_behalf_of": "660e8400-e29b-41d4-a716-446655440001"}

Required parameters

ParameterDescription
source_currencyStablecoin you will send: USDC or USDT
target_currencyFiat the beneficiary receives: GBP, EUR, or CAD (when enabled on your account)
source_amountStablecoin amount to convert (decimal string)
beneficiary_idFiat beneficiary from Step 2
quote_type"DIRECT_EXCHANGE"
payment_method"CRYPTO"
payment_networkNetwork for funding (e.g. POLYGON, SOL, ETH) — must match what you send on
origin_referenceYour unique idempotency reference (max 40 characters)
on_behalf_ofCustomer user id from Step 1

Success response (201)

json
{  "quote_id": "abc12345-e89b-12d3-a456-426614174000",  "source": { "currency": "USDC", "amount": "250.00" },  "target": { "currency": "GBP", "amount": "197.50" },  "exchange_rate": "0.79",  "fees": {    "processing_fee": "1.00",    "deposit_fee": "0.50",    "payout_fee": "0.75",    "total_fees": "2.25",    "fee_currency": "USDC"  },  "total_payable": "252.25",  "valid_until": "2026-03-16T15:40:53Z",  "quote_status": "CREATED",  "type": "DIRECT_EXCHANGE"}

Step 4: Accept the quote — receive payment instructions

Accepting locks the quote and returns payment instructions. For crypto offramp, Sznd returns the crypto deposit address mapped to your fiat beneficiary, along with the exact amount and 15-minute funding deadline for this transaction.

The address belongs to the beneficiary's fiat destination — if you offramp to the same bank account again, you will typically receive the same address, but you must still accept each new quote and use the new amount and deadline from that accept response.

POST /api/v1/client/quotes/{quote_id}/accept

Success response (200)

json
{  "quote_id": "abc12345-e89b-12d3-a456-426614174000",  "transaction_id": "def67890-e21b-43d2-b456-426614174111",  "transaction_reference": "TXN-OFR-12345",  "status": "AWAITING_PAYMENT",  "total_payable": "252.25",  "timeline": {    "created_at": "2026-03-16T15:10:53Z",    "valid_until": "2026-03-16T15:25:53Z"  },  "payment_details": {    "quote_id": "abc12345-e89b-12d3-a456-426614174000",    "type": "BLOCKCHAIN",    "blockchain_payment_info": {      "payment_network": "Polygon",      "payment_address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",      "payment_token": "USDC",      "funding_deadline": "2026-03-16T15:25:53Z",      "funding_window_minutes": 15,      "payment_instructions": "Send exactly 252.25 USDC on Polygon to this Sznd deposit address within 15 minutes: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e"    }  }}

Fields to use in your product UI

FieldPurpose
payment_details.blockchain_payment_info.payment_addressCrypto address your user or treasury must send to
payment_details.blockchain_payment_info.payment_tokenToken to send (USDC / USDT)
payment_details.blockchain_payment_info.payment_networkNetwork to use — must match the deposit
total_payableExact amount to send (includes fees where applicable)
funding_deadline / timeline.valid_untilDeadline — funding must arrive before this time
funding_window_minutesAlways 15 for crypto offramp — show a countdown to your user

The crypto address is mapped to the fiat beneficiary, not generated arbitrarily per quote. Always read payment_address, total_payable, and funding_deadline from the current accept response.


Step 5: Send stablecoin within the funding window

  1. Send total_payable of payment_token on the specified payment_network to payment_address.
  2. Complete the transfer within 15 minutes of accept (funding_deadline).
  3. Use the exact token and network returned — sending the wrong asset or chain can result in loss of funds.

Your business treasury or the customer may send the crypto. Sznd attributes the deposit to the quote you accepted for this transaction, using the amount and deadline from that accept response.

After Sznd detects the deposit, fiat payout to the beneficiary bank account is processed automatically.


Step 6: Webhook and reconciliation

Subscribe to transaction webhooks. You will receive updates as the quote moves from payment confirmed through payout to completed.

Example completion payload (shape):

json
{  "event_type": "transaction",  "transaction_id": "def67890-e21b-43d2-b456-426614174111",  "transaction_type": "DIRECT_EXCHANGE",  "status": "COMPLETED",  "reference": "TXN-OFR-12345",  "user_id": "660e8400-e29b-41d4-a716-446655440001",  "source_currency": "USDC",  "source_amount": "250.00",  "target_currency": "GBP",  "target_amount": "197.50",  "amount_with_fees": "252.25",  "recipient": {    "account_number": "12345678",    "account_name": "Alex Rivera",    "bank_name": "Example Bank",    "beneficiary_id": "550e8400-e29b-41d4-a716-446655440000"  }}

Reconcile using origin_reference, transaction_reference, and quote_id.


Complete cURL example

bash
# 1) Create beneficiary (after customer is registered and verified)curl -X POST https://api.sznd.app/api/v1/client/beneficiaries \  -H "Content-Type: application/json" \  -H "x-api-key: YOUR_API_KEY" \  -H "x-timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" \  -H "x-signature: YOUR_SIGNATURE" \  -d '{    "user_id": "660e8400-e29b-41d4-a716-446655440001",    "account_name": "Alex Rivera",    "email": "customer.a@example.com",    "phone": "+447700900123",    "bank_name": "Example Bank",    "account_number": "12345678",    "sort_code": "123456",    "country_code": "GB",    "currency_code": "GBP"  }'
# 2) Create DIRECT_EXCHANGE quotecurl -X POST https://api.sznd.app/api/v1/client/quotes \  -H "Content-Type: application/json" \  -H "x-api-key: YOUR_API_KEY" \  -H "x-timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" \  -H "x-signature: YOUR_SIGNATURE" \  -d '{    "source_currency": "USDC",    "target_currency": "GBP",    "source_amount": "250.00",    "beneficiary_id": "550e8400-e29b-41d4-a716-446655440000",    "quote_type": "DIRECT_EXCHANGE",    "payment_method": "CRYPTO",    "payment_network": "POLYGON",    "tz": "UTC",    "origin_reference": "OFFRAMP-2026-0042",    "on_behalf_of": "660e8400-e29b-41d4-a716-446655440001"  }'
# 3) Accept quote — receive crypto address mapped to this fiat payoutcurl -X POST https://api.sznd.app/api/v1/client/quotes/QUOTE_ID/accept \  -H "Content-Type: application/json" \  -H "x-api-key: YOUR_API_KEY" \  -H "x-timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" \  -H "x-signature: YOUR_SIGNATURE"

Best practices

  1. Register the customer first — Offramp quotes require a verified customer (on_behalf_of).
  2. Separate beneficiary from crypto address — Register bank details once; Sznd maps a crypto deposit address to that fiat beneficiary. Accept each quote to get the amount and deadline for that transaction.
  3. Show the 15-minute countdown — Use funding_deadline in your UI; expired quotes need a new accept (and new amount/deadline).
  4. Send exact amount and network — Use total_payable, payment_token, and payment_network from the accept response only — never from a previous quote.
  5. Unique origin_reference — Prevents duplicate quotes for the same business intent.
  6. Webhooks over polling — Handle AWAITING_PAYMENTCOMPLETED asynchronously with idempotent webhook handlers.

Crypto offramp vs other flows

FlowQuote typeSourceWhat you receive on accept
Crypto offrampDIRECT_EXCHANGEUSDC / USDTCrypto address (mapped to beneficiary) + amount + 15 min deadline → fiat to beneficiary
Direct exchange (fiat)DIRECT_EXCHANGEFiat (USD, NGN, …)Bank / rail payment instructions
Crypto collectionDEPOSITUSDC / USDTCrypto address → credit your wallet
Fiat payoutWITHDRAWALWallet balanceNo deposit step — debits your wallet

Related documentation