Skip to main content
Bitnob sends a webhook to your server when something happens on your account: a deposit lands, a payout completes, a card is charged. Set your webhook URL and webhook secret in the dashboard. Some requests, such as Create card and Initialize payout, also take a per-request URL.

Verify every delivery

Each delivery 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 SHA-512 of the raw request body, keyed with the webhook secret from your dashboard. Your API client secret doesn’t sign webhooks.
  • x-bitnob-event-id: the event’s unique ID, also in the body. Use it to de-duplicate.
To verify a delivery:
  1. Read the request body as raw bytes, before any JSON parsing.
  2. Compute the HMAC SHA-512 of those bytes with your webhook secret, and hex-encode it.
  3. Compare it with x-bitnob-signature in constant time (crypto.timingSafeEqual, hmac.compare_digest, hmac.Equal). If the header is missing or doesn’t match, respond 401 and do nothing else.
  4. Parse the JSON, skip the event if you’ve already processed its ID, and respond 200.
Don’t parse the body and serialize it again before checking. JSON.stringify, json.dumps, and json_encode change spacing, key order, and escaping. Bitnob sends <, >, and & as \u003c, \u003e, and \u0026, so a re-serialized body passes for some events and fails for others.

Retries and duplicates

Respond as soon as the signature checks out, then process the event in the background. Bitnob reads your status code like this:
If your own processing fails temporarily, for example your database is down, respond with a 5xx so the event is retried. A 400 or 422 drops it for good.
  • Expect duplicates. The same event can arrive more than once, even after you returned 2xx. De-duplicate on event_id.
  • Don’t assume order. A retried delivery can land after a later event. Use the status in the payload, not arrival order, to decide what happened.
  • Reconcile on a schedule. Retries are finite, so an endpoint that’s down for long enough misses events for good. Check the relevant list endpoint for anything you still hold as pending.

Your webhook secret

You set the webhook secret in the dashboard under Settings → Webhooks. Keep it on your server only, never in client-side code or source control. Change it if it may have been exposed, and deploy the new secret to your receiver as close as you can to the moment you change it in the dashboard. Deliveries that arrive while the two don’t match fail your signature check. Your 401 is retried, so they’re redelivered as long as the switch finishes within the retry window; reconcile anything that still goes missing.

Complete Python receiver

A ready-to-run receiver with no third-party packages, for Python 3.8 or later. It:
  • 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 processed only after your handler succeeds. If the handler fails, it responds 500, so the retry is processed rather than skipped.
  • Includes a self-test: python bitnob_webhook.py --self-test.
Save it as bitnob_webhook.py, replace handle_event with your own logic, and start it with BITNOB_WEBHOOK_SECRET=your_webhook_secret python bitnob_webhook.py --port 8000. It listens on POST /webhooks/bitnob. In production, store processed event IDs in your database rather than in memory, and run the receiver behind HTTPS.
bitnob_webhook.py

Source IPs

If your firewall restricts inbound traffic, allow the addresses Bitnob sends webhooks from:
An IP allowlist isn’t a security boundary. Any host can send a request from an allowlisted address, and these addresses can change. Verify the signature on every event, whether or not you filter by IP.

Test your endpoint

Webhooks need a public HTTPS URL. While you build, expose your local server with a tunnel such as ngrok, and set the tunnel URL as your webhook URL in the sandbox dashboard. Only do this in sandbox. To check your verification code without waiting for a real event, sign a sample body with your webhook secret and post it yourself. The signed request should get 200. Change one character of the body without re-signing, and it should get 401.

Events by product