---
url: /en/guide/webhooks.md
---
# 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](./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 `FAILED` and delivery stops.
* **The same event can arrive more than once** (retries, manual resends). Dedupe by `event_id` and 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=FAILED` lists undelivered events — process their `data`;
* `POST /v1/events/{event_id}/resend` redelivers immediately to your current webhook URL (same `event_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`.
