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

# Verifying signatures

> Check that a webhook request really comes from ReyGrid and hasn't been tampered with.

Your endpoint URL is public, so anyone could send it a request. Every ReyGrid delivery is signed with your endpoint's **signing secret**. Verify the signature before you trust the payload.

## The signature header

```text theme={"system"}
X-ReyGrid-Signature: t=1791190000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

| Part | Description |
| - | - |
| `t` | Unix timestamp (seconds) of when the request was signed |
| `v1` | Hex HMAC-SHA256 of the signed payload, keyed with your signing secret |

The **signed payload** is the timestamp, a dot, and the **raw** request body:

```text theme={"system"}
{t}.{raw body}
```

## Verify a request

<Steps>
  <Step title="Read the raw body">
    Use the exact bytes you received. Parsing the JSON and serializing it again changes whitespace and key order, and the signature won't match.
  </Step>

  <Step title="Split the header">
    Split `X-ReyGrid-Signature` on `,` then each part on `=` to get `t` and `v1`.
  </Step>

  <Step title="Compute the expected signature">
    HMAC-SHA256 of `{t}.{raw body}` with your signing secret (the full `whsec_…` string), as lowercase hex.
  </Step>

  <Step title="Compare and check the age">
    Compare with `v1` using a **constant-time** comparison, and reject requests whose `t` is more than 5 minutes away from your clock to block replays.
  </Step>
</Steps>

<CodeGroup>
  ```javascript Node.js theme={"system"}
  import crypto from "node:crypto";

  const TOLERANCE_SECONDS = 5 * 60;

  /**
   * @param {string} header  The X-ReyGrid-Signature header
   * @param {string} body    The raw request body
   * @param {string} secret  Your endpoint's signing secret (whsec_…)
   */
  export function verifyReyGrid(header, body, secret) {
    if (!header) return false;
    const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
    const timestamp = Number(parts.t);
    if (!timestamp || !parts.v1) return false;

    if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;

    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.${body}`)
      .digest("hex");

    const received = Buffer.from(parts.v1, "hex");
    const computed = Buffer.from(expected, "hex");
    return received.length === computed.length && crypto.timingSafeEqual(received, computed);
  }
  ```

  ```python Python theme={"system"}
  import hashlib
  import hmac
  import time

  TOLERANCE_SECONDS = 5 * 60


  def verify_reygrid(header: str, body: bytes, secret: str) -> bool:
      """header: X-ReyGrid-Signature, body: raw request body, secret: whsec_…"""
      try:
          parts = dict(part.split("=", 1) for part in header.split(","))
          timestamp = int(parts["t"])
          received = parts["v1"]
      except (KeyError, ValueError):
          return False

      if abs(time.time() - timestamp) > TOLERANCE_SECONDS:
          return False

      signed = f"{timestamp}.".encode() + body
      expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
      return hmac.compare_digest(received, expected)
  ```
</CodeGroup>

## Framework notes

<AccordionGroup>
  <Accordion title="Express">
    Use `express.raw({ type: "application/json" })` on the webhook route (not `express.json()`), then `req.body.toString("utf8")` is the raw body.
  </Accordion>

  <Accordion title="Next.js (App Router)">
    ```javascript theme={"system"}
    export async function POST(request) {
      const body = await request.text(); // raw body
      const signature = request.headers.get("x-reygrid-signature");
      if (!verifyReyGrid(signature, body, process.env.REYGRID_WEBHOOK_SECRET)) {
        return new Response("Invalid signature", { status: 400 });
      }
      const event = JSON.parse(body);
      // handle event…
      return new Response(null, { status: 200 });
    }
    ```
  </Accordion>

  <Accordion title="Flask / FastAPI">
    Flask: `request.get_data()`. FastAPI: `await request.body()`. Both return the raw bytes to pass to `verify_reygrid`.
  </Accordion>
</AccordionGroup>

## Rotating the secret

Select the rotate button next to the secret on the endpoint card in **Console → Webhooks**. The new secret applies to the **next** delivery immediately, so update your server's environment right after rotating.

<Warning>
  Treat the signing secret like a password: keep it server-side, never commit it, and rotate it if it leaks.
</Warning>

## Best practices

* **Respond fast.** Return `2xx` as soon as the signature checks out and process the event in the background. Requests time out after 10 seconds.
* **Be idempotent.** Retries reuse the same event `id` (also in `X-ReyGrid-Delivery`). Skip IDs you've already processed.
* **Don't rely on order.** Events can arrive out of order. Use `created` when ordering matters.
* **Ignore unknown types.** Return `2xx` for event types you don't handle, so new events never cause failures.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.