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

# Verify webhook signatures

> Verify the Standard Webhooks signature against the exact request body before parsing it.

Verify each webhook in two steps: read the raw body, then call `unwrap` with the request headers.

## Before you start

* Save `signing_secret` from the subscription's [create response](/webhooks/subscriptions#store-the-signing-secret) as `RELAY_WEBHOOK_SECRET`.
* Read the raw body before any JSON middleware touches it; changed whitespace changes the signed bytes.

## Verify with the SDK

`unwrap` checks the three webhook headers against the raw body and returns the parsed event. It throws when a header is missing, the signature does not match, or the body is not JSON. Configure the secret once on the client, or pass `key` per call when one receiver serves several subscriptions:

```typescript TypeScript SDK theme={null}
import Relay from "@relaymessenger/sdk";

const relay = new Relay({
  apiKey: process.env.RELAY_AGENT_TOKEN!,
  baseURL: "https://api.relayapp.im",
  webhookSecret: process.env.RELAY_WEBHOOK_SECRET!,
});

const rawBody = await request.text();
const event = relay.webhooks.unwrap(rawBody, {
  headers: request.headers,
});
```

| SDK input or method     | Use                                                                    |
| ----------------------- | ---------------------------------------------------------------------- |
| `rawBody`               | Unmodified request body as a string or Buffer                          |
| `headers`               | Request `Headers`, or a record with the lowercase webhook header names |
| `webhookSecret`         | Default signing secret configured on the client                        |
| `key`                   | Optional per-call secret for `unwrap` or `verify`                      |
| `relay.webhooks.verify` | Verify without returning a parsed event                                |
| `relay.webhooks.unwrap` | Verify, then parse the JSON envelope                                   |

## Read the signature format

Relay signs requests with Standard Webhooks, so any Standard Webhooks library verifies them too. The signing input joins the ID and the timestamp with periods and appends the raw body. Relay strips the `whsec_` prefix, base64-decodes the key, and computes HMAC-SHA256 over that input:

```text theme={null}
webhook-id.webhook-timestamp.raw-body
```

| Header              | Value                             |
| ------------------- | --------------------------------- |
| `webhook-id`        | Stable event ID                   |
| `webhook-timestamp` | Attempt timestamp as Unix seconds |
| `webhook-signature` | `v1,<base64-signature>`           |

A retry reuses the event ID and body under a new timestamp, so deduplicate by `event_id`, never by the signature.

## When it fails

Reject the request with `401` before you store anything when a header is missing or the signature does not match; [delivery policy](/webhooks/delivery#handle-retries) treats that as terminal. Reject a body that passes the signature but does not parse the same way. If your secret is missing, restore it from your secret store; the [receiver example](/webhooks#receive-and-acknowledge) returns `401` for every case.

## Next steps

* [Receive events](/webhooks)
* [Manage subscriptions](/webhooks/subscriptions)
* [Handle delivery and retries](/webhooks/delivery)
* [Debug with trace IDs](/live/debugging)
