Webhook Verification
Every webhook Bitnob sends is signed, so your server can prove an event came from Bitnob and was not changed on the way. Verify the signature on every event before you act on it: a forged event could otherwise credit a customer, mark a payout as complete, or release goods.
This page applies to every Bitnob webhook: withdrawals, deposits to addresses, payouts, virtual accounts, cards and trading. Each product's webhook page lists its events and payloads.
What Bitnob sends
Each event is an HTTPS POST with a JSON body of the form {"data": {...}, "event": "...", "event_id": "..."} and two headers:
- x-bitnob-signature: the hex-encoded HMAC-SHA512 of the raw request body, keyed with your webhook's signing secret.
- x-bitnob-event-id: the event's unique ID, also in the body as event_id. Use it to de-duplicate.
Your signing secret
You set the signing secret when you create a webhook on the dashboard under Settings → Webhooks. Keep it on your server only, never in client-side code or source control, and change it if it may have been exposed.
Verify the Signature
1. Read the raw body
Read the request body as raw bytes, exactly as received, before any JSON parsing. The signature covers those exact bytes.
Parsing the JSON and serialising it again (JSON.stringify, json.dumps, json_encode) changes spacing, key order and escaping. For example, Bitnob sends <, > and & as \u003c, \u003e and \u0026. The signature will then fail for some events and pass for others.
2. Compute the expected signature
Compute the HMAC-SHA512 of the raw body, using your signing secret as the key, and hex-encode the result.
3. Compare in constant time
Compare your result with the x-bitnob-signature header using a constant-time comparison (crypto.timingSafeEqual, hmac.compare_digest, hmac.Equal), not ==. If the header is missing or doesn't match, respond 401 and do nothing else.
4. Parse, de-duplicate, then respond 200
Only after the signature matches, parse the JSON. Skip the event if you have already processed its event_id. Respond 200 quickly and do slow work in the background.
Retries and De-duplication
How Bitnob treats your response:
- Any 2xx status means the event was delivered. Bitnob does not send it again.
- 400, 404, 410, 422 or 451 means you rejected the event permanently. Bitnob stops and does not retry, so only return these when retrying would never help.
- Anything else (5xx, 401, 403, 408, 429, a timeout or no response) is retried with increasing delays (by default about 1, 5, 15 and 60 minutes), up to the retry count set on the webhook: 1 to 5 delivery attempts in total. If you send a Retry-After header, Bitnob waits that long before retrying (capped).
If your own processing fails temporarily, for example your database is down, respond with a 5xx so the event is retried.
- The same event can arrive more than once, even after you returned 200. Record each processed event_id and skip repeats.
- Events can arrive out of order. Use the status in the payload, not arrival order, to decide what happened.
- Don't rely on webhooks alone. Reconcile by querying the API for transactions in case deliveries keep failing.
Complete Python Receiver
A ready-to-run receiver. It needs Python 3.8 or later and no third-party packages.
- Verifies the signature over the raw body with a constant-time comparison.
- Responds 401 to a missing or wrong signature, and 200 to a duplicate without processing it again.
- Marks an event as processed only after your handler succeeds. If the handler fails, it responds 500, so Bitnob's retry is processed rather than skipped.
- Includes a self-test that checks all of the above against a payload signed by Bitnob.
Replace handle_event with your own logic. In production, store processed event IDs in your database rather than in memory, and run the receiver behind HTTPS.
Testing Your Endpoint
Your endpoint must be reachable on a public HTTPS URL. While developing, expose your local server with a tunnel such as ngrok and use the tunnel URL for a sandbox webhook only.
To check your verification code without waiting for a real event, sign a sample body yourself with your signing secret and post it to your endpoint. A correctly signed request should get 200; the same request with the body changed should get 401.
You can also restrict your endpoint to Bitnob's webhook source IPs, listed on each product's webhook page. Always verify the signature as well: the signature is the check that proves the event is genuine.