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

# Verifying Signatures

> Authenticate incoming webhook deliveries using the HMAC signature and timestamp headers.

Every webhook request is signed with your subscription's **signing secret** (returned once when the subscription is created or its secret is regenerated). Verify the signature on every request before trusting the payload — this proves the request came from Riverside and was not tampered with.

## Request headers

| Header                  | Example            | Description                                           |
| :---------------------- | :----------------- | :---------------------------------------------------- |
| `x-riverside-signature` | `v1=8b2e...f0`     | Versioned HMAC signature of the request.              |
| `x-riverside-timestamp` | `1752595283`       | Unix timestamp (seconds) when the request was signed. |
| `content-type`          | `application/json` | The body is always JSON.                              |

## How the signature is computed

```text theme={null}
signed_string = "{x-riverside-timestamp}:{raw_request_body}"
signature     = "v1=" + HMAC_SHA256(signing_secret, signed_string)   // hex
```

The signature is prefixed with `v1=` to allow future scheme changes. Compute your own HMAC over the **raw** request body (the exact bytes received — do not re-serialize the parsed JSON) and compare it to the value in `x-riverside-signature`.

## Verification steps

1. Read the raw request body **before** any JSON parsing.
2. Read `x-riverside-timestamp` and `x-riverside-signature`.
3. Reject the request if the timestamp is too old (e.g. more than 5 minutes) to guard against replay attacks.
4. Compute `v1=HMAC_SHA256(secret, "{timestamp}:{rawBody}")`.
5. Compare against the received signature using a **constant-time** comparison.
6. Only then parse the body and process the event.

## Example (Node.js / Express)

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

const SIGNING_SECRET = process.env.RIVERSIDE_WEBHOOK_SECRET;
const MAX_SKEW_SECONDS = 5 * 60;

if (!SIGNING_SECRET) {
  throw new Error("RIVERSIDE_WEBHOOK_SECRET is required");
}

const app = express();

// Capture the raw body — the signature is computed over the exact bytes.
app.use("/hooks/riverside", express.raw({ type: "*/*" }));

app.post("/hooks/riverside", (req, res) => {
  const signature = req.header("x-riverside-signature") ?? "";
  const timestamp = Number(req.header("x-riverside-timestamp"));
  const rawBody = req.body.toString("utf8");

  // 1. Reject stale requests (replay protection).
  if (!Number.isFinite(timestamp) ||
      Math.abs(Date.now() / 1000 - timestamp) > MAX_SKEW_SECONDS) {
    return res.status(400).send("stale timestamp");
  }

  // 2. Recompute and compare in constant time.
  const expected =
    "v1=" +
    crypto
      .createHmac("sha256", SIGNING_SECRET)
      .update(`${timestamp}:${rawBody}`)
      .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send("bad signature");
  }

  // 3. Verified — parse and handle. Dedupe on event.id.
  const event = JSON.parse(rawBody);
  // ... enqueue / process event ...

  res.status(200).send("ok");
});
```

<Warning>
  Never verify against the re-serialized JSON — whitespace and key ordering differ from the bytes we signed and the check will fail. Always sign/verify the raw body.
</Warning>

<Tip>
  Acknowledge fast: respond `2xx` as soon as you've verified and durably enqueued the event, then process asynchronously. Slow endpoints hit the delivery timeout and get [retried](/webhooks/delivery-and-retries).
</Tip>
