Skip to main content

Send a withdrawal

  1. Once: register the destination and allowlist the asset:

    POST /v1/external-wallets
    { "label": "treasury-cold" }

    POST /v1/external-wallets/{id}/assets
    { "assetCanonical": "USDT_ETHEREUM", "address": "0xfeed...beef" }

    Wait until the asset entry is active (some providers require allowlist approval first).

  2. Send, referencing the wallet:

    POST /v1/transfers/external
    Idempotency-Key: <uuid>
    { "srcAccountId": "8f1c...", "assetCanonical": "USDT_ETHEREUM", "amount": "250.00",
    "destination": { "externalWalletId": "b2d4..." }, "note": "payout #4821" }

Option B — one-time address

Skip the allowlist and pass a raw address:

POST /v1/transfers/external
Idempotency-Key: <uuid>
{ "srcAccountId": "8f1c...", "assetCanonical": "USDT_ETHEREUM", "amount": "10.00",
"destination": { "address": "0xfeed...beef", "memo": null } }

Track it to completion

The response is a transfer in signing. Follow transfer.* webhooks to completed (or failed). The lifecycle is:

createdsigningbroadcastingconfirmingcompleted

Handle held & rejected

If your workspace runs policy in enforce mode:

  • The transfer may return in state held — it is not submitted to the provider until an approver releases it in the console. Treat held as "pending approval," and let the transfer.* webhooks tell you when it moves on.
  • A rule (or screening) may reject it with 403 and a "Transfer blocked by policy: …" message. Surface that to your user; it won't be retried automatically.

:::warning Always send an Idempotency-Key Without one, a network retry creates a second withdrawal. With one, ZenVault returns the original transfer on replay. See Idempotency. :::