Skip to main content

Webhooks API

Register endpoints to receive events instead of polling.

MethodPathScope
POST/v1/webhook-endpointswebhook-endpoints:write
GET/v1/webhook-endpointswebhook-endpoints:read
GET/v1/webhook-endpoints/{id}webhook-endpoints:read
PATCH/v1/webhook-endpoints/{id}webhook-endpoints:write
POST/v1/webhook-endpoints/{id}/rotate-secretwebhook-endpoints:write
DELETE/v1/webhook-endpoints/{id}webhook-endpoints:write

Register an endpoint

POST /v1/webhook-endpoints
{ "url": "https://app.example.com/webhooks/zenvault", "eventTypes": ["transfer.*", "address.received"] }

eventTypes filters what's delivered: empty/omitted means all events; entries match exactly or as a prefix wildcard (transfer.* matches transfer.completed, transfer.failed, …).

The response includes oneTimeSecretthe HMAC signing key, returned exactly once. Store it; you verify every delivery with it. rotate-secret issues a new one and invalidates the old.

:::warning HTTPS required Webhook URLs must use HTTPS — plaintext http:// is rejected with 400 on create/update for production workspaces (which today is every workspace). :::

Event types

TypeFires when
transfer.createdTransfer recorded
transfer.signingBeing signed
transfer.broadcastingSubmitted to the network
transfer.confirmingSeen on-chain, awaiting confirmations
transfer.completedConfirmed / final
transfer.failedFailed
transfer.cancelledCancelled
transfer.timed_outTimed out
address.receivedAn inbound deposit to one of your addresses confirmed
gas.lowGas wallet dropped below the refill threshold (Gas)
gas.depletedGas wallet can't fund top-ups — web3 withdrawals will stall

Delivery payload

Every delivery is a POST with this envelope:

{
"id": "evt_3f2a...uuid",
"type": "transfer.completed",
"occurred_at": "2026-05-27T10:12:00.000Z",
"workspace_id": "...",
"data": { ... }
}

data for transfer.*:

{
"id": "d91a...uuid",
"kind": "external",
"state": "completed",
"asset_canonical": "USDT_ETHEREUM",
"amount": "250.00",
"src_account_id": "8f1c...",
"dest_account_id": null,
"dest_external_address": "0xfeed...beef"
}

data for address.received (emitted once, on confirmation):

{
"transfer_id": "a17c...uuid",
"destination_account_id": "8f1c...",
"destination_address": "0x9a8b...c3d4",
"asset_canonical": "USDT_ETHEREUM",
"amount": "125.50",
"tx_hash": "0x6f4a..."
}

Headers & retries

HeaderValue
zenvault-event-idevt_<uuid> — stable across retries; dedupe on it
zenvault-event-typee.g. transfer.completed
zenvault-delivery-attempt1, 2, …
zenvault-signaturet=<unix-seconds>,v1=<hex-hmac>

Respond 2xx to acknowledge. Any non-2xx (or timeout — we wait 15s) is retried with jittered exponential backoff (capped at 6h) up to 18 attempts over ~3 days, after which the delivery is marked dead.

:::info At-least-once, unordered Deliveries are at-least-once and unordered. Make handlers idempotent (dedupe on zenvault-event-id) and treat the transfer state machine as the source of truth rather than assuming events arrive in order. :::

Always verify the signature — see Verify webhooks.