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

# Webhook verifier

> Cryptographic verifier for AlgoVoi webhook signatures: v1 HMAC-SHA256 and v2 HKDF-SHA256 with HMAC-SHA384, offline capable, Python and TypeScript.

[![Keystone Integration](https://img.shields.io/badge/Keystone-integration-7c8aa0)](/keystone)

The AlgoVoi gateway signs every outbound webhook with an `X-AlgoVoi-Signature` header containing a UNIX timestamp and two HMAC components. `algovoi-webhook-verifier` validates the header, enforces replay protection, and returns the parsed event — with no runtime dependency on AlgoVoi infrastructure.

```bash theme={null}
pip install algovoi-webhook-verifier
```

***

## Verification steps

| Step | Check                                                                                            |
| ---- | ------------------------------------------------------------------------------------------------ |
| 1    | `X-AlgoVoi-Signature` header present                                                             |
| 2    | Header matches `t=<unix>,v1=<sha256hex>[,v2=<sha384hex>]`                                        |
| 3    | Timestamp within tolerance window (default 300 s)                                                |
| 4    | v1 = HMAC-SHA256(secret, `"{ts}." + raw_body`)                                                   |
| 5    | v2 = HMAC-SHA384(HKDF-SHA256(secret, salt, info), `"{ts}." + raw_body`) — validated when present |
| 6    | Body is valid JSON object                                                                        |
| 7    | `type` field is a known event type                                                               |

The v2 component uses HKDF-SHA256 key derivation with `salt=b"algovoi-webhook-v2-pqc"` and `info=b"hmac-sha384-outbound"`, length 48 bytes.

***

## Quick start

<CodeGroup>
  ```python Python theme={null}
  from algovoi_webhook_verifier import verify_webhook, WebhookVerificationError

  def handle_webhook(raw_body: bytes, signature_header: str, secret: str):
      try:
          event = verify_webhook(
              payload=raw_body,
              secret=secret,
              signature_header=signature_header,
          )
      except WebhookVerificationError as e:
          # e.code is one of the six typed error codes
          return {"error": e.code}, 400

      if event["type"] == "payment.confirmed":
          data = event["data"]
          print(f"Payment confirmed: {data['resource_id']} on {data['chain']}")
      return {"ok": True}, 200
  ```

  ```typescript TypeScript theme={null}
  import { verifyWebhook, WebhookVerificationError } from "@algovoi/webhook-verifier";

  function handleWebhook(rawBody: Buffer, signatureHeader: string, secret: string) {
    try {
      const event = verifyWebhook({
        payload: rawBody,
        secret,
        signatureHeader,
      });

      if (event.type === "payment.confirmed") {
        const { resource_id, chain } = event.data as Record<string, string>;
        console.log(`Payment confirmed: ${resource_id} on ${chain}`);
      }
      return { ok: true };
    } catch (err) {
      if (err instanceof WebhookVerificationError) {
        return { error: err.code };
      }
      throw err;
    }
  }
  ```
</CodeGroup>

***

## Framework integration

<CodeGroup>
  ```python Flask theme={null}
  from flask import Flask, request
  from algovoi_webhook_verifier import verify_webhook, WebhookVerificationError
  import os

  app = Flask(__name__)

  @app.route("/webhook", methods=["POST"])
  def webhook():
      try:
          event = verify_webhook(
              payload=request.get_data(),
              secret=os.environ["ALGOVOI_WEBHOOK_SECRET"],
              signature_header=request.headers.get("X-AlgoVoi-Signature", ""),
          )
      except WebhookVerificationError as e:
          return {"error": e.code}, 400

      # process event["type"]
      return {"received": True}, 200
  ```

  ```typescript Express theme={null}
  import express from "express";
  import { verifyWebhook, WebhookVerificationError } from "@algovoi/webhook-verifier";

  const app = express();
  // Important: use raw body parser, not json()
  app.use("/webhook", express.raw({ type: "application/json" }));

  app.post("/webhook", (req, res) => {
    try {
      const event = verifyWebhook({
        payload: req.body,
        secret: process.env.ALGOVOI_WEBHOOK_SECRET!,
        signatureHeader: req.headers["x-algovoi-signature"] as string,
      });
      res.json({ received: true });
    } catch (err) {
      if (err instanceof WebhookVerificationError) {
        res.status(400).json({ error: err.code });
      } else {
        res.status(500).end();
      }
    }
  });
  ```
</CodeGroup>

***

## Error codes

| Code                  | Cause                                         | HTTP suggestion |
| --------------------- | --------------------------------------------- | --------------- |
| `MISSING_SIGNATURE`   | Header absent or blank                        | 400             |
| `MALFORMED_SIGNATURE` | Header does not match format                  | 400             |
| `STALE_SIGNATURE`     | Timestamp outside tolerance window            | 400             |
| `INVALID_SIGNATURE`   | HMAC mismatch — tampered body or wrong secret | 401             |
| `INVALID_PAYLOAD`     | Body is not a valid JSON object               | 400             |
| `UNKNOWN_EVENT_TYPE`  | `type` field not in known set                 | 400             |

***

## API reference

### Python

```python theme={null}
verify_webhook(
    *,
    payload: bytes,
    secret: str,
    signature_header: str,
    tolerance: int = 300,      # seconds; 0 disables staleness check
    require_v2: bool = False,  # require v2 component present and valid
) -> dict
```

Raises `WebhookVerificationError(code, message)` on any failure. `.code` is one of the six `ErrorCode` literals. `.message` is a human-readable description.

### TypeScript

```typescript theme={null}
verifyWebhook(options: VerifyOptions): WebhookEvent

interface VerifyOptions {
  payload: Buffer | Uint8Array | string;
  secret: string;
  signatureHeader: string;
  tolerance?: number;    // default 300; 0 to disable
  requireV2?: boolean;   // default false
}
```

Throws `WebhookVerificationError` with `.code` (typed `ErrorCode`) and `.message`.

***

## Webhook event shape

```json theme={null}
{
  "id": "evt_01abc...",
  "type": "payment.confirmed",
  "created": 1748000000,
  "api_version": "2024-01-01",
  "data": {
    "tenant_label": "my-shop",
    "resource_id": "pay_xyz...",
    "chain": "base",
    "asset": {
      "id": "usdc",
      "label": "USDC",
      "decimals": 6
    },
    "amount_microunits": 1000000,
    "amount_pretty": "1.00 USDC",
    "tx_id": "0x...",
    "payer_address": "0x...",
    "payment_link_token": "tok_...",
    "payment_link_label": "Checkout"
  }
}
```

***

## Supported event types

| Type                | Description                                           |
| ------------------- | ----------------------------------------------------- |
| `payment.confirmed` | On-chain payment confirmed by the AlgoVoi facilitator |

Additional event types will be added in future versions with full vector coverage.

***

## Cross-validation vectors

13 fixtures in `vectors/valid/` (5) and `vectors/invalid/` (8). Each vector is self-contained — it embeds the secret, raw body, and header so any language implementation can verify itself against the same corpus.

| Vector                          | Error code            |
| ------------------------------- | --------------------- |
| `v01_payment_confirmed_v1v2`    | — (valid)             |
| `v02_payment_confirmed_v1_only` | — (valid)             |
| `v03_different_secret`          | — (valid)             |
| `v04_minimal_payload`           | — (valid)             |
| `v05_unicode_payload`           | — (valid)             |
| `i01_missing_signature`         | `MISSING_SIGNATURE`   |
| `i02_malformed_signature`       | `MALFORMED_SIGNATURE` |
| `i03_stale_signature`           | `STALE_SIGNATURE`     |
| `i04_invalid_signature_v1`      | `INVALID_SIGNATURE`   |
| `i05_wrong_secret`              | `INVALID_SIGNATURE`   |
| `i06_tampered_body`             | `INVALID_SIGNATURE`   |
| `i07_invalid_payload_not_json`  | `INVALID_PAYLOAD`     |
| `i08_unknown_event_type`        | `UNKNOWN_EVENT_TYPE`  |

Regenerate all vectors:

```bash theme={null}
python vectors/generate_vectors.py
```

***

## Test results

| Implementation     | Tests | Result |
| ------------------ | ----- | ------ |
| Python unit        | 34    | 34/34  |
| Python vectors     | 13    | 13/13  |
| TypeScript unit    | 32    | 32/32  |
| TypeScript vectors | 13    | 13/13  |

**Python 47/47 · TypeScript 45/45**

### 8-language cross-validation

**104/104** agreements — all 8 language implementations produce byte-for-byte identical HMAC results and identical error-code verdicts across all 13 vectors.

| Language   | Vectors |
| ---------- | ------- |
| Python     | 13/13   |
| TypeScript | 13/13   |
| Go         | 13/13   |
| Rust       | 13/13   |
| Java       | 13/13   |
| PHP        | 13/13   |
| .NET       | 13/13   |
| Ruby       | 13/13   |

Attestation and reproduction commands: [`_attestations/2026-05-31-8-impl-cross-validation.md`](https://github.com/chopmob-cloud/algovoi-webhook-verifier/blob/master/_attestations/2026-05-31-8-impl-cross-validation.md)

***

## See also

* [Notifications](/concepts/notifications) — webhook delivery, retry schedule, and secret rotation
* [Compliance receipt verifier](/receipt-verifier) — JWS compliance receipt verification
* [Audit verifier](/audit-verifier) — offline audit bundle verification
* [Package suite](/package-suite) — full open-source package listing
