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-IdThe event id, the same as on the cursor. Deduplicate on it
X-Bankcore-TimestampUnix seconds
X-Bankcore-Signaturev1=<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

Kinds

Events are outcomes you can act on, not every transition.

KindResource
deposit.detecteddepositAn arrival was recorded
deposit.confirmeddepositIt reached the asset's confirmation depth
deposit.reorgeddepositIts block was replaced; the coins are not there
chain_payout.settledoperationA payout landed
chain_payout.failedoperationA payout will not land
chain_payout.unresolvedoperationNot yet known. Never read it as either
withdrawal.settled / .failed / .unresolvedoperationA withdrawal to your verified address
swap.settled / .failed / .unresolvedoperationA conversion
payout.*, transfer.*operationFiat payouts and transfers
deposit.settledoperationA 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.