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

# Webhooks

# `blursec.webhooks`

Verify HMAC-SHA256-signed deliveries Blursec posts to your endpoint.

A credential or session can come back **clean at login** and only show up in a
stealer-log batch hours or days later. Webhooks are how Blursec tells you about
those *after-the-fact* compromises (e.g. `session.compromised`,
`credential.leaked`) so you can revoke proactively instead of waiting for the
next check.

Verification is **local** — it never round-trips to the Blursec API, so it
works in any runtime and adds no latency to your handler.

```ts theme={null}
import { Blursec } from "@blursec/sdk";

const blursec = new Blursec({ apiKey: process.env.BLURSEC_API_KEY! });
```

***

## `verify(options) → Promise<boolean>`

Returns `true` only when **both** the signature is valid **and** the delivery
falls inside the replay window. Returns `false` for any malformed or stale
delivery — it never throws on a bad delivery, so a misbehaving sender can't
crash your handler. It throws `BlursecValidationError` only when `secret` is
empty (caller misconfiguration).

> **Pass the raw request body.** The signature is computed over the exact
> bytes Blursec sent. Any JSON re-serialization or whitespace normalization by
> your framework will invalidate it — use a raw-body parser on the webhook
> route.

```ts theme={null}
import express from "express";

app.post(
  "/webhooks/blursec",
  express.raw({ type: "*/*" }),          // raw bytes, not express.json()
  async (req, res) => {
    const ok = await blursec.webhooks.verify({
      secret: process.env.BLURSEC_WEBHOOK_SECRET!,
      payload: req.body,                 // Buffer of the exact bytes
      signature: req.header("X-Blursec-Signature") ?? "",
      timestamp: req.header("X-Blursec-Timestamp") ?? "",
    });

    if (!ok) return res.status(401).end();

    const event = JSON.parse(req.body.toString("utf8"));
    // ...handle event.type: "session.compromised", "credential.leaked", ...
    res.status(204).end();
  },
);
```

### `VerifyWebhookOptions`

```ts theme={null}
interface VerifyWebhookOptions {
  secret: string;                                  // shared webhook secret
  payload: string | Uint8Array | ArrayBuffer;      // raw, unparsed body bytes
  signature: string;                               // X-Blursec-Signature (hex, optional "sha256=" prefix)
  timestamp: string | number;                      // X-Blursec-Timestamp (Unix epoch ms)
  toleranceMs?: number;                            // replay window; default 5 min
}
```

***

## Delivery headers

Blursec sets these headers on every delivery. They're also exported as a typed
bag (`BLURSEC_WEBHOOK_HEADERS`) and surfaced on `blursec.webhooks.headerNames`
so framework adapters wire them up consistently:

| Header                | Meaning                                         |
| --------------------- | ----------------------------------------------- |
| `X-Blursec-Signature` | `sha256=<hex>` HMAC of the body + timestamp     |
| `X-Blursec-Timestamp` | Unix epoch **milliseconds** of the delivery     |
| `X-Blursec-Delivery`  | Unique id for this delivery (idempotency key)   |
| `X-Blursec-Event`     | Semantic event name, e.g. `session.compromised` |

The default replay-protection window is **5 minutes**
(`DEFAULT_WEBHOOK_TOLERANCE_MS`, also on `blursec.webhooks.defaultToleranceMs`).
Override it per call with `toleranceMs`.

***

## Standalone verification (no client)

`verify` is a thin wrapper over the exported `verifyWebhookSignature` helper.
Use the helper directly in an edge handler that doesn't otherwise need a
`BlursecClient`:

```ts theme={null}
import {
  verifyWebhookSignature,
  BLURSEC_WEBHOOK_HEADERS,
} from "@blursec/sdk";

export default async function handler(req: Request): Promise<Response> {
  const body = await req.text();
  const ok = await verifyWebhookSignature({
    secret: BLURSEC_WEBHOOK_SECRET,
    payload: body,
    signature: req.headers.get(BLURSEC_WEBHOOK_HEADERS.signature) ?? "",
    timestamp: req.headers.get(BLURSEC_WEBHOOK_HEADERS.timestamp) ?? "",
  });
  return new Response(null, { status: ok ? 204 : 401 });
}
```

For tests, `signWebhookPayload(...)` produces a valid signature for a given
secret + payload + timestamp so you can exercise your handler end-to-end.
