Send a withdrawal
Option A — allowlisted wallet (recommended for recurring destinations)
-
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). -
Send, referencing the wallet:
POST /v1/transfers/externalIdempotency-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. Treatheldas "pending approval," and let thetransfer.*webhooks tell you when it moves on. - A rule (or screening) may reject it with
403and 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. :::