Events & webhooks
The cursor is the contract. Webhooks are the fast path. Every event is a row you can fetch from GET /v1/events, and a webhook is a delivery of that same row with the same id.
The cursor
GET /v1/events?after=0&limit=100
Authorization: Bearer <your key>
{
"events": [
{
"id": 8412,
"kind": "deposit.confirmed",
"resource_id": "3f1c2a9e-...",
"occurred_at": "2026-09-26T08:22:48Z",
"data": {"...": "..."}
}
],
"next_after": 8412
}
Ids increase monotonically. Store the last id you processed and ask for what came after it. limit defaults to 100, maximum 500. next_after is null on an empty page. Scope: events:read.
Webhooks
Give us an HTTPS URL and we POST each event to it:
{
"id": 8412,
"type": "deposit.confirmed",
"resource": {"type": "deposit", "id": "3f1c2a9e-..."},
"occurred_at": "2026-09-26T08:22:48Z",
"data": {"...": "..."}
}
The envelope differs from the cursor: type here is kind there, and resource.id here is resource_id there. Map both into one handler.
| Header | |
|---|---|
X-Bankcore-Event-Id | The event id, the same as on the cursor. Deduplicate on it |
X-Bankcore-Timestamp | Unix seconds |
X-Bankcore-Signature | v1=<hex>: HMAC-SHA256 with the shared secret we give you, over <timestamp>.<raw body> |
Verify before you trust the body: compare in constant time and reject a timestamp more than 300 seconds old.
import hashlib, hmac, time
def verified(secret: bytes, timestamp: str, body: bytes, signature: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(secret, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(f"v1={expected}", signature)
Delivery
- Any
2xxis success. Anything else, including a redirect, is a failure. - 8 attempts: the first at once, then retries after 10 s, 30 s, 2 min, 10 min, 30 min, 1 h, 1 h. After that the event is abandoned and not resent. It stays on
GET /v1/events, which is what makes abandoning it safe. - Without a webhook URL every event is still recorded on the cursor.
- Order is not guaranteed. Deduplicate and process idempotently.
Kinds
Events are outcomes you can act on, not every transition.
| Kind | Resource | |
|---|---|---|
deposit.detected | deposit | An arrival was recorded |
deposit.confirmed | deposit | It reached the asset's confirmation depth |
deposit.reorged | deposit | Its block was replaced; the coins are not there |
chain_payout.settled | operation | A payout landed |
chain_payout.failed | operation | A payout will not land |
chain_payout.unresolved | operation | Not yet known. Never read it as either |
withdrawal.settled / .failed / .unresolved | operation | A withdrawal to your verified address |
swap.settled / .failed / .unresolved | operation | A conversion |
payout.*, transfer.* | operation | Fiat payouts and transfers |
deposit.settled | operation | A deposit credited to your balance |
A chain payout, a withdrawal and a fiat payout are different products with different events: subscribing to one does not deliver another.
On deposit events data carries deposit_id, network, address, reference, currency, amount, amount_units, tx_hash, transfer_index, block_number, confirmations, required_confirmations, status, historical and change_seq. Here amount is a bare decimal string, not an object.
On payout events data carries operation_id, kind, status, amount, currency, provider_reference and tx_hash. Your own reference is not in it: reconcile on operation_id. tx_hash is filled on chain_payout.settled; on unresolved, and on a failed payout that was already signed, the transaction is in provider_reference.
amount on an event may be written at full scale ("500.000000000000000000") where the API writes "500". Compare as decimals.