Webhooks (events)
When a deposit is credited or a withdrawal changes state, we POST an event to your webhook URL.
Event format
json
{
"event_id": "evt_1987654321098754",
"type": "deposit.confirmed",
"created_at": "1762650000000",
"data": { "deposit_id": "1987654321098752", "status": "CONFIRMED", "amount": "12.5", "...": "..." }
}data is identical to the response of the matching GET endpoint, as a snapshot at event time:
| type | When | data equals |
|---|---|---|
deposit.confirmed | The deposit reached the required confirmations and was credited | GET /v1/deposits/{deposit_id} |
withdraw.broadcasted | The withdrawal transaction was broadcast and awaits confirmation | GET /v1/withdraws/{withdraw_id} |
withdraw.confirmed | The withdrawal is confirmed (final); in-pool transfers go straight here | same |
withdraw.failed | The withdrawal failed (final); see data.reason; for pooled merchants the amount frozen at creation is released | same |
withdraw.rejected | The withdrawal was rejected in review (final); see data.reason; for pooled merchants the amount frozen at creation is released | same |
New event types may be added — return 2xx and ignore types you don't recognise.
Headers and signature
| Header | Description |
|---|---|
X-Webhook-Id | Equals event_id |
X-Timestamp | Time of this delivery attempt, Unix milliseconds (changes on every retry) |
X-Signature | hex(HMAC-SHA256(webhook secret, X-Timestamp + "\n" + body)) |
Verify against the raw body bytes you received — don't parse and re-serialize the JSON first. Also reject timestamps more than 5 minutes from your clock to prevent replays.
Test vector: webhook secret whsec-test, X-Timestamp: 1762650000000, body
json
{"event_id":"evt_1","type":"deposit.confirmed","created_at":"1762650000000","data":{}}Expected signature: 31c194e933a5f62c752bace322b571f8e281edf589c97c5b672ac245e8a54664. See Code examples for implementations.
Acknowledgement, retries and deduplication
- Any 2xx response counts as success; the body is ignored. Each attempt times out after 30 seconds.
- On failure (non-2xx, timeout, connection error) each round retries immediately up to 3 times, then backs off until the next round — 1 s, 3 s, 10 s, 30 s, 2 min, 5 min, 10 min, 30 min, 2 h, 6 h, then every 12 h. After 15 rounds (about 3 days) the event is marked
FAILEDand delivery stops. - The same event can arrive more than once (retries, manual resends). Dedupe by
event_idand keep your handler idempotent. - Events can arrive out of order. Trust
data.status; query the GET endpoint for the latest state when needed. - Return 2xx quickly and do slow work asynchronously.
Recovering missed events
GET /v1/events?delivery_status=FAILEDlists undelivered events — process theirdata;POST /v1/events/{event_id}/resendredelivers immediately to your current webhook URL (sameevent_id);- if a deposit has neither a webhook nor a record (e.g. the chain feed missed it), rescan it by transaction hash with
POST /v1/deposits/rescan.