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

# Receive and Verify Blockra Webhook Event Notifications

> Register an HTTPS endpoint to receive real-time Blockra payment events. Includes signature verification examples in Node.js and Python.

Blockra sends a `POST` request to your registered HTTPS endpoint whenever a notable event occurs on a payment or payout. React to confirmations, expirations, and payouts in real time without polling the API. Every delivery is signed so you can verify it genuinely came from Blockra.

## Setting up a webhook endpoint

<Steps>
  <Step title="Expose an HTTPS endpoint">
    Create a route on your server that accepts `POST` requests with a JSON body, reachable over HTTPS, that returns a `2xx` response quickly.
  </Step>

  <Step title="Register the endpoint">
    In the Blockra dashboard, go to **Developers → Webhooks** (or call [`POST /webhooks`](/api-reference/create-webhook)) and add your endpoint URL plus the events to subscribe to. Blockra returns a signing secret (prefixed `whsec_`) **once**, on creation — copy it and store it securely. You need it to verify signatures.
  </Step>

  <Step title="Verify and handle events">
    Verify the `X-Blockra-Signature` header on each delivery before trusting the payload (see below), then return `2xx`.
  </Step>
</Steps>

## Events

Subscribe to any of these when you register an endpoint. Each delivery also carries the event name in an `X-Blockra-Event` header.

| Event                | When it fires                                                    |
| -------------------- | ---------------------------------------------------------------- |
| `payment.created`    | A payment was created                                            |
| `payment.confirming` | Funds were seen on-chain; waiting for the required confirmations |
| `payment.completed`  | Payment confirmed; your balance was credited                     |
| `payment.expired`    | The 30-minute quote expired before funds arrived                 |
| `payout.completed`   | A withdrawal was confirmed on-chain                              |
| `payment.refunded`   | Reserved — not currently emitted                                 |

## Payload structure

Every delivery uses the same envelope: the event `type`, a `created` Unix timestamp (seconds), and a `data.object` holding the resource — a payment, or a withdrawal for `payout.completed`.

```json payment.completed theme={null}
{
  "type": "payment.completed",
  "created": 1783515120,
  "data": {
    "object": {
      "id": "pay_a1b2c3d4e5f6g7h8i9j0k1l2",
      "status": "completed",
      "asset": "LTC",
      "network": "litecoin",
      "fiat_currency": "USD",
      "fiat_amount": 9.99,
      "fee_fiat": 0.22,
      "net_fiat": 9.77,
      "reference": "order_1234",
      "created_at": "2026-07-08T12:22:00.000Z",
      "completed_at": "2026-07-08T12:52:00.000Z"
    }
  }
}
```

<Note>
  `data.object` is the same shape returned by [`GET /payments/{id}`](/api-reference/get-payment) (or the withdrawal object for `payout.completed`), including integer `*_atoms` amount fields for exactness.
</Note>

## Verifying signatures

Every delivery includes an `X-Blockra-Signature` header of the form:

```text theme={null}
X-Blockra-Signature: t=1783515180,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

* `t` — the Unix timestamp (seconds) when the delivery was signed.
* `v1` — the hex HMAC-SHA256, keyed with your endpoint's signing secret, over the timestamp `t`, a literal `.`, and the **raw request body** (that is, `t` + `"."` + body).

Recompute `v1` and compare it in constant time. Optionally reject deliveries whose `t` is more than a few minutes old to guard against replay.

<Warning>
  Sign over the **raw** request body, before any JSON parsing or re-serialisation. Reformatting the body changes the bytes and invalidates the signature.
</Warning>

<CodeGroup>
  ```ts Node.js theme={null}
  import { createHmac, timingSafeEqual } from "node:crypto";

  function verify(rawBody: string, header: string, secret: string): boolean {
    const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
    const { t, v1 } = parts;
    if (!t || !v1) return false;
    // Optional replay guard: reject if the signature is older than 5 minutes.
    if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
    const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
    const a = Buffer.from(expected);
    const b = Buffer.from(v1);
    return a.length === b.length && timingSafeEqual(a, b);
  }

  // Express — mount with a raw body parser so you verify the exact bytes.
  app.post("/webhooks/blockra", express.raw({ type: "application/json" }), (req, res) => {
    const sig = req.headers["x-blockra-signature"] as string;
    if (!verify(req.body.toString(), sig, process.env.BLOCKRA_WEBHOOK_SECRET!)) {
      return res.status(400).send("Invalid signature");
    }
    const event = JSON.parse(req.body.toString());
    // handle event.type and event.data.object
    res.sendStatus(200);
  });
  ```

  ```python Python theme={null}
  import hmac, hashlib, os, time
  from flask import Flask, request, abort

  app = Flask(__name__)

  def verify(raw_body: bytes, header: str, secret: str) -> bool:
      parts = dict(p.split("=", 1) for p in header.split(","))
      t, v1 = parts.get("t"), parts.get("v1")
      if not t or not v1:
          return False
      # Optional replay guard: reject if older than 5 minutes.
      if abs(time.time() - int(t)) > 300:
          return False
      signed = f"{t}.".encode() + raw_body
      expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, v1)

  @app.route("/webhooks/blockra", methods=["POST"])
  def blockra_webhook():
      sig = request.headers.get("X-Blockra-Signature", "")
      if not verify(request.get_data(), sig, os.environ["BLOCKRA_WEBHOOK_SECRET"]):
          abort(400, "Invalid signature")
      event = request.get_json()
      # handle event["type"] and event["data"]["object"]
      return "", 200
  ```
</CodeGroup>

## Responding to deliveries

Return a `2xx` status code quickly — within a few seconds — then do any slow work asynchronously. A non-`2xx` response or a timeout is retried up to 6 times with backoff: 1 minute, 5 minutes, 30 minutes, 2 hours, then 6 hours. After the final attempt the delivery is marked failed.

<Tip>
  Make your handler **idempotent** — key off the event `type` plus `data.object.id`. Blockra may deliver the same event more than once.
</Tip>
