> ## Documentation Index
> Fetch the complete documentation index at: https://relay-demo.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify signatures

> Check that a webhook came from Relay and wasn't changed or replayed.

Anyone who finds your endpoint's URL can send it requests. Every request from Relay carries a signature made with your endpoint's signing secret, which only you and Relay know. Check it before you act on the event.

The signing secret starts with `whsec_`. It is shown once when you create the endpoint; find or roll it later under **Webhooks → your endpoint → Signing secret**.

## How it works

![Sequence diagram between Relay and the customer's webhook server: Relay computes HMAC-SHA256 over the timestamp and raw body with the signing secret and sends it in the Relay-Signature header; the server reads the raw body, rejects timestamps older than 5 minutes, recomputes the HMAC, compares it in constant time, and answers 200 on a match or 400 otherwise.](https://mintcdn.com/relay-demo/g4EzaOdLYNrmsxJ6/images/webhook-signature.png?fit=max\&auto=format\&n=g4EzaOdLYNrmsxJ6\&q=85\&s=cf6d3e4bc9592c110732fbaafab08988)

Relay signs the timestamp and the raw body with the endpoint's secret. Your server checks the timestamp, recomputes the signature and compares.

The header holds a timestamp and the signature:

```text theme={null}
Relay-Signature: t=1729600000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

1. Split the header on `,` and read `t` and `v1`.
2. Reject the request if `t` is more than 5 minutes from your server's clock. This stops someone resending a request they captured.
3. Compute HMAC-SHA256 of `{t}.{raw body}` with your signing secret, as lowercase hex.
4. Compare it with `v1` in constant time. Answer `400` if they differ.

## Use the raw body

Sign and compare the bytes exactly as they arrived. If your framework parses JSON first, the re-serialized body can differ by a space or a key order and the signature won't match. In Express, use `express.raw({ type: "application/json" })` on the webhook route.

## Example

```js Node theme={null}
import crypto from "node:crypto";

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (age > 300) throw new Error("timestamp too old");

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

  const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
  if (!ok) throw new Error("signature mismatch");
  return JSON.parse(rawBody);
}
```

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

def verify(raw_body: bytes, header: str, secret: str):
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        raise ValueError("timestamp too old")

    signed = f'{parts["t"]}.'.encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, parts["v1"]):
        raise ValueError("signature mismatch")
    return json.loads(raw_body)
```

The [Node SDK](https://relay-demo.mintlify.site/sdks/node.md) does this in `relay.webhooks.verify`.

## Rolling the secret

When you roll a secret, Relay signs with both the old and the new one for 24 hours and sends two `v1` values in the header. Accept the request if either matches, and deploy the new secret within the day.


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