Flows

Withdrawal Workflow

End-to-end guide to crypto withdrawals: initiate on a network, receive the verification code by webhook, confirm, and track the withdrawal until it settles on-chain.

Sending crypto out is the most sensitive thing an account does, so every withdrawal takes two steps: you request it, then you confirm it with a verification code. Nothing leaves the platform until the code checks out, and the withdrawal only counts as done when it settles on-chain.

Authentication: every endpoint in this guide accepts your Owner API Key (x-api-key) or the user's token (Authorization: Bearer). Each endpoint is documented in full on Withdrawals.

Overview

  1. Initiate the withdrawal with currency, amount, address and network. It starts PENDING.
  2. Receive the verification code in the user.withdrawal.verification_code_issued webhook event.
  3. Confirm with the code. The withdrawal moves to PROCESSING.
  4. Read the withdrawal until it's COMPLETED or FAILED.

Prerequisites:

  • An Owner API Key (x-api-key) or the user's token (Authorization: Bearer).
  • An active user with enough available balance in the currency.
  • A webhook endpoint configured in Customer webhooks to receive the code.

Step-by-Step Guide

Step 1: Initiate the withdrawal

Send the currency, a decimal-string amount, the destination address and the network. network is required because one asset can live on several networks. Add an x-idempotency-key so a retry after a timeout returns the same withdrawal instead of creating another.

POSThttps://api.sandbox.sovera.io/sovx/v1/users/:user_id/accounts/crypto/withdrawals
curl -X POST "https://api.sandbox.sovera.io/sovx/v1/users/5299e81c-3a8c-5b64-a112-b93bf43de74b/accounts/crypto/withdrawals" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-idempotency-key: 9a7c3e1f-5b2d-4c8e-a6f0-1d3b5e7f9a2c" \
  -d '{
    "currency": "BTC",
    "amount": "0.01000000",
    "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
    "network": "BITCOIN",
    "memo": "Withdrawal to hardware wallet"
  }'

The response is the withdrawal in PENDING with a verification block. Keep the id: every later call uses it.

{
  "success": true,
  "data": {
    "id": "3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d",
    "object": "withdrawal",
    "atx_id": "3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d",
    "status": "PENDING",
    "status_reason": null,
    "user_id": "5299e81c-3a8c-5b64-a112-b93bf43de74b",
    "customer_id": "corp-redundant-firewall",
    "wallet_id": null,
    "debited_wallets": [],
    "currency": "BTC",
    "amount": "0.01000000",
    "fee": null,
    "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
    "network": "BITCOIN",
    "memo": "Withdrawal to hardware wallet",
    "tx_hash": null,
    "idempotency_key": "9a7c3e1f-5b2d-4c8e-a6f0-1d3b5e7f9a2c",
    "created_at": "2026-10-12T14:03:11Z",
    "updated_at": "2026-10-12T14:03:11Z",
    "completed_at": null,
    "created_by": "12015942-4d29-5733-a774-14ee16e31da3",
    "updated_by": "12015942-4d29-5733-a774-14ee16e31da3",
    "verification": {
      "expires_at": "2026-10-12T14:08:11Z",
      "attempts_remaining": 3
    }
  },
  "meta": {
    "timestamp": "2026-10-12T14:03:11Z",
    "version": "v1",
    "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}

Step 2: Receive the verification code

Sovera never returns the code in an API response. Your webhook endpoint receives it in the user.withdrawal.verification_code_issued event, with withdrawal_id, user_id, code, expires_at, trigger, attempts_remaining, amount, currency, network and destination_address. The code has 6 digits, lasts 5 minutes and allows 3 attempts. When the treasury has to be replenished first, the code reaches Sovera by email: it then lasts 15 minutes from the email, the event always says trigger: "issued" and has no attempts_remaining, and custody counts the attempts. See the full event in Webhooks.

Show the code to your user to approve the withdrawal, or confirm it from your backend if your own flow already authorized it.

Step 3: Confirm with the code

POSThttps://api.sandbox.sovera.io/sovx/v1/users/:user_id/accounts/crypto/withdrawals/:withdrawal_id/confirm
curl -X POST "https://api.sandbox.sovera.io/sovx/v1/users/5299e81c-3a8c-5b64-a112-b93bf43de74b/accounts/crypto/withdrawals/3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d/confirm" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{ "code": "482913" }'

A valid code returns the withdrawal in PROCESSING; once the funds are debited, the wallets are listed in debited_wallets (it can still be empty when the treasury is being replenished first). The funds are on their way, but the transaction isn't settled yet. If Sovera sent the request but lost the answer, the withdrawal is PROCESSING with status_reason.code EXECUTION_OUTCOME_UNKNOWN: don't confirm again. Read the withdrawal until it settles: no webhook announces the final status, and a failure is announced by ledger.wallet.credited with reason withdrawal_reversal when the funds come back.

  • Wrong code: 400 INVALID_VERIFICATION_CODE, and the message says how many attempts remain.
  • Third wrong code: 403 VERIFICATION_BLOCKED. The withdrawal is BLOCKED; cancel it and start again.
  • Code lapsed: 422 INVALID_STATE_TRANSITION (or 400 VERIFICATION_CODE_EXPIRED). The withdrawal is EXPIRED; initiate a new one.

Step 3b (optional): Resend the code

Lost the code, or it's about to lapse? Request a new one while the withdrawal is still PENDING. A new event arrives with trigger: "resent" (issued when the code comes by email), and the old code stops working.

POSThttps://api.sandbox.sovera.io/sovx/v1/users/:user_id/accounts/crypto/withdrawals/:withdrawal_id/verification-codes
curl -X POST "https://api.sandbox.sovera.io/sovx/v1/users/5299e81c-3a8c-5b64-a112-b93bf43de74b/accounts/crypto/withdrawals/3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d/verification-codes" \
  -H "x-api-key: YOUR_API_KEY"

Step 4: Track the withdrawal until it settles

Read the withdrawal until status is COMPLETED or FAILED. Records are permanent, so you can read it at any time.

GEThttps://api.sandbox.sovera.io/sovx/v1/users/:user_id/accounts/crypto/withdrawals/:withdrawal_id
curl -X GET "https://api.sandbox.sovera.io/sovx/v1/users/5299e81c-3a8c-5b64-a112-b93bf43de74b/accounts/crypto/withdrawals/3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d" \
  -H "x-api-key: YOUR_API_KEY"
  • COMPLETED: tx_hash, completed_at and the real network fee ({ amount, currency, paid_by }) are set; fee stays null if the network didn't report an exact amount.
  • FAILED: status_reason explains why, e.g. { "code": "NETWORK_FAILED", "message": "The network rejected the transaction." }.

To find withdrawals in flight, list them with a status filter:

GET https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}/accounts/crypto/withdrawals?filter[where][status][inq][]=PENDING&filter[where][status][inq][]=PROCESSING

Cancel (optional)

Cancel a PENDING or BLOCKED withdrawal before it's confirmed. It turns CANCELLED and nothing is debited. A PROCESSING withdrawal can't be cancelled.

POSThttps://api.sandbox.sovera.io/sovx/v1/users/:user_id/accounts/crypto/withdrawals/:withdrawal_id/cancel
curl -X POST "https://api.sandbox.sovera.io/sovx/v1/users/5299e81c-3a8c-5b64-a112-b93bf43de74b/accounts/crypto/withdrawals/3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d/cancel" \
  -H "x-api-key: YOUR_API_KEY"

Troubleshooting

The code never arrives

  • Check that your webhook endpoint is configured and verified in Customer webhooks.
  • Keep the deposit email poller configured too: when the treasury needs replenishing first, the code reaches Sovera by email before it's forwarded to you.
  • Resend the code while the withdrawal is PENDING.

422 VALIDATION_ERROR on /network

The network is missing or the currency doesn't run on it. The message lists the networks the currency supports, e.g. network must be one of: ETHEREUM for USDT.

400 INSUFFICIENT_BALANCE

The user's available balance in that currency is below the amount. Check the user balance first.

403 FORBIDDEN

The account is frozen, or a user token tried to act on another user.

422 INVALID_STATE_TRANSITION

The withdrawal isn't in a status that allows the action, e.g. confirming a withdrawal that's already PROCESSING. Read it to see its current status.

503 SERVICE_UNAVAILABLE

Crypto withdrawals aren't enabled for your account yet. Contact support.

Best Practices

  1. Always send network. Never assume a default network for an asset.
  2. Send an x-idempotency-key on initiate and confirm, and reuse it on retries.
  3. Treat PROCESSING as in flight, not done. Only COMPLETED means the funds settled.
  4. Never log or expose the code. Pass it only to the user who approves the withdrawal.
  5. Verify the destination address before you initiate.

Next Steps

Support

Need help? Email [email protected]

On this page