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

# Verify signatures

> Authenticate every delivery before trusting it.

Every delivery carries these headers:

| Header                | Contents                                  |
| --------------------- | ----------------------------------------- |
| `X-Traddal-Event`     | The event type (`consignment.created`, …) |
| `X-Traddal-Delivery`  | Unique delivery ID — dedupe key           |
| `X-Traddal-Timestamp` | Unix timestamp of the send                |
| `X-Traddal-Signature` | `v1=<hex HMAC>`                           |

The signature is an HMAC-SHA256 over the timestamp and the **raw** request
body, keyed with your endpoint's secret (`whsec_…`, shown on the Developers
page):

```text theme={null}
hex = HMAC_SHA256(secret, "{X-Traddal-Timestamp}.{rawBody}")
```

Reject the delivery when the recomputed HMAC differs, or when the timestamp
is older than a few minutes (replay guard).

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

  export function verifyTraddalWebhook(
    rawBody: string,
    headers: Record<string, string>,
    secret: string,
  ): boolean {
    const timestamp = headers["x-traddal-timestamp"];
    const signature = headers["x-traddal-signature"]; // "v1=<hex>"
    if (!timestamp || !signature?.startsWith("v1=")) return false;

    // Replay guard: reject anything older than 5 minutes.
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

    const expected = createHmac("sha256", secret)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");
    const given = signature.slice(3);
    return (
      expected.length === given.length &&
      timingSafeEqual(Buffer.from(expected), Buffer.from(given))
    );
  }
  ```

  ```python Python theme={null}
  import hashlib, hmac, time

  def verify_traddal_webhook(raw_body: bytes, headers: dict, secret: str) -> bool:
      timestamp = headers.get("x-traddal-timestamp", "")
      signature = headers.get("x-traddal-signature", "")
      if not timestamp or not signature.startswith("v1="):
          return False
      if abs(time.time() - float(timestamp)) > 300:  # replay guard
          return False
      expected = hmac.new(
          secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, signature[3:])
  ```
</CodeGroup>

<Warning>
  Compute the HMAC over the **raw** body bytes exactly as received. Parsing and
  re-serializing the JSON will change key order or whitespace and break the
  signature.
</Warning>

Rotating the secret (Developers page or
`POST /admin/api/v1/settings/webhooks/{id}/rotate-secret`) invalidates old
signatures immediately.
