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

# Verify Webhook Requests

> Authenticate every delivery with HMAC-SHA256 signatures, timestamps, and constant-time comparison.

Never trust an inbound POST just because it hits your webhook path. Every delivery is signed with your endpoint's secret — reject anything with a missing or invalid signature before parsing the body.

## Headers

| Header             | Example                     | Meaning                                           |
| :----------------- | :-------------------------- | :------------------------------------------------ |
| `X-Gork-Signature` | `t=1758573296,v1=9a8b7c6d…` | Timestamp plus hex HMAC (see below)               |
| `X-Gork-Event`     | `email.received`            | Event name — route on this                        |
| `X-Gork-Delivery`  | `del_9f2c41ab77d0…`         | Stable id per delivery attempt — dedupe on this   |
| `X-Gork-Attempt`   | `1`                         | Attempt number (1–5)                              |
| `X-Gork-Timestamp` | `1758573296`                | Unix seconds; duplicates the `t` in the signature |

## Signature scheme

* `v1` = `HEX(HMAC_SHA256(secret, "{t}.{rawJSON}"))` — the timestamp, a literal `.`, and the exact raw request body bytes.
* Always verify against the **raw body**, not a re-serialized object — whitespace differences break the HMAC.
* Reject signatures older than **±300 seconds** to close the replay window.

<Warning>
  Compare digests in constant time (`timingSafeEqual` / `hmac.compare_digest`) and check the length first — a naive equality check leaks the secret to timing attacks, and some compare functions throw instead of returning `false` on length mismatch.
</Warning>

## SDK helpers (recommended)

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { verifyGorkWebhook } from "@gork/sdk";

  export async function POST(req: Request) {
    const raw = await req.text(); // raw bytes — do not JSON.parse first
    const ok = await verifyGorkWebhook({
      payload: raw,
      signature: req.headers.get("x-gork-signature"),
      secret: process.env.GORK_WEBHOOK_SECRET!,
    });
    if (!ok) return new Response("Unauthorized", { status: 401 });

    const event = JSON.parse(raw);
    // ... handle event, deduping on req.headers.get("x-gork-delivery")
    return new Response("OK", { status: 200 });
  }
  ```

  ```python Python theme={null}
  from gork import verify_signature

  raw = await request.body()  # exact bytes received
  ok = verify_signature(
      raw.decode("utf-8"),
      request.headers["X-Gork-Signature"],
      os.environ["GORK_WEBHOOK_SECRET"],
  )
  if not ok:
      return Response("Unauthorized", status_code=401)
  ```
</CodeGroup>

Both helpers enforce the ±300s tolerance and constant-time comparison (`tolerance_seconds` / `toleranceSeconds` is configurable).

## Manual verification

If you cannot use the SDKs, the algorithm is three steps — split the header, check freshness, recompute and compare:

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

  function verifyGorkWebhookManual(raw: string, header: string | null, secret: string): boolean {
    if (!header) return false;
    const parts = Object.fromEntries(
      header.split(",").map((p) => p.split("=", 2)),
    );
    const t = parts["t"];
    const v1 = parts["v1"];
    if (!t || !v1 || !/^[0-9a-f]+$/i.test(v1)) return false;
    if (Math.abs(Date.now() / 1000 - parseInt(t, 10)) > 300) return false;

    const expected = createHmac("sha256", secret).update(`${t}.${raw}`).digest("hex");
    if (expected.length !== v1.length) return false;
    return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
  }
  ```

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

  def verify_gork_webhook_manual(raw: str, header: str, secret: str) -> bool:
      try:
          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
          if abs(time.time() - int(t)) > 300:
              return False
          expected = hmac.new(secret.encode(), f"{t}.{raw}".encode(), hashlib.sha256).hexdigest()
          return hmac.compare_digest(v1, expected)
      except Exception:
          return False
  ```
</CodeGroup>
