> ## Documentation Index
> Fetch the complete documentation index at: https://www.bitnob.dev/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Bitnob API requests authenticate with an `X-Auth-Client` API key header against https://api.bitnob.com.
> Always use the sandbox environment for testing. Never put live API keys in sample code.
> Money amounts are integers in the smallest unit of the currency (satoshis for BTC, cents for fiat). Never use floats.
> Every state-changing request takes a client-supplied `reference` and must be treated as idempotent on retry.
> Quotes expire. Re-quote rather than reusing a stale quote id.

# Receive card webhooks

> Card events arrive as signed HTTP POSTs. Verify the HMAC-SHA512 signature on the raw body, de-duplicate on event_id, and respond fast.

Card events are delivered as HTTP POST requests to the webhook URL you set on your dashboard or per card at creation. Each delivery is signed, retried on failure, and possibly duplicated, so a correct handler does three things: verify, de-duplicate, acknowledge.

| Header | Value |
| - | - |
| `Content-Type` | `application/json` |
| `x-bitnob-signature` | HMAC-SHA512 of the raw body, keyed with your webhook secret |

The payload envelope and the full event catalog are on [Webhook events](https://www.bitnob.dev/docs/card-issuing/webhooks.md).

## Verify the signature

Compute HMAC-SHA512 over the **raw request body** using your webhook secret, and compare it to `x-bitnob-signature` in constant time. Reading the parsed JSON and re-serializing it will not match; verify the exact bytes received.

```python Python (Flask) theme={null}
import hmac
import hashlib
import json
from flask import Flask, request, abort

app = Flask(__name__)

BITNOB_SECRET = b'your_webhook_secret_here'

def verify_signature(raw_body, signature):
    expected = hmac.new(BITNOB_SECRET, raw_body, hashlib.sha512).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route("/webhook", methods=["POST"])
def handle_webhook():
    raw_body = request.get_data()
    signature = request.headers.get("x-bitnob-signature", "")

    if not verify_signature(raw_body, signature):
        abort(401, "Invalid signature")

    # Parse the raw body yourself: request.json rejects the
    # "application/json, application/json" Content-Type deliveries can carry
    event = json.loads(raw_body)
    print(f"Verified event: {event['event']}")
    return "", 200
```

```javascript Node.js (Express) theme={null}
const crypto = require('crypto');
const express = require('express');
const app = express();

const BITNOB_SECRET = 'your_webhook_secret_here';

// Read the raw bytes whatever Content-Type says: deliveries can arrive as
// "application/json, application/json", which type: 'application/json' rejects
app.post('/webhook', express.raw({ type: () => true }), (req, res) => {
  const rawBody = req.body;
  const signature = req.headers['x-bitnob-signature'] || '';

  const expected = crypto
    .createHmac('sha512', BITNOB_SECRET)
    .update(rawBody)
    .digest('hex');

  const valid =
    expected.length === signature.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));

  if (!valid) return res.status(401).send('Invalid signature');

  const event = JSON.parse(rawBody.toString('utf-8'));
  console.log(`Verified event: ${event.event}`);
  res.status(200).send();
});

app.listen(3000);
```

Reference implementation and secrets guidance: [Verifying events](https://www.bitnob.dev/api-reference/webhooks.md#verify-every-delivery).

## Handle delivery semantics

| Behavior | What it means for your handler |
| - | - |
| Retries | Any `2xx` counts as delivered. `400`, `404`, `410`, `422`, and `451` are final and never retried. Anything else, including a timeout, is retried with increasing delays, up to the retry count set on your webhook (1 to 5 attempts in total). Return `200` as soon as you have accepted the event and process asynchronously; if processing fails temporarily, return a `5xx`. See [Retries and duplicates](https://www.bitnob.dev/api-reference/webhooks.md#retries-and-duplicates). |
| Duplicates | Retries reuse the same `event_id`. Persist processed IDs and ignore repeats. |
| Ordering | Do not assume order. Branch on `event`, then `transaction_type`, and reconcile against [card transactions](https://www.bitnob.dev/api-reference/cards/list-card-transactions-by-card.md) rather than inferring state from sequence. |
| New fields | Event names are stable, but new optional fields can appear in `data` without a version bump. Ignore what you do not recognize. |

If your firewall restricts inbound traffic, allowlist the published [Source IPs](https://www.bitnob.dev/api-reference/webhooks.md#source-ips), but only as a second layer: any host can spoof a source address, so the signature is the check that counts. Always use HTTPS, and keep the secret out of client code and repositories.

## Test your endpoint

* Trigger real test events end to end from the sandbox environment.
* Replay captured payloads with `curl` or Postman against your local handler, signing them with your test secret.
* Tunnel to localhost with a tool like ngrok while developing, and keep tunnels out of production configuration.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.