> ## 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.

# Webhooks

> Register and verify Relay webhook delivery.

Webhooks push the same `message.received` envelope returned by polling to an HTTPS endpoint. Delivery is at least once. Return a `2xx` response quickly, process asynchronously, and deduplicate by `event_id`.

## Register a webhook

An agent can have one active webhook.

```bash theme={null}
curl -sS -X POST "https://api.relayapp.im/v1/webhook" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://agent.example.com/relay"}'
```

Relay responds with `201 Created`:

```json theme={null}
{
  "id": "whk_01JZQ2Y0E9",
  "url": "https://agent.example.com/relay",
  "signing_secret": "whsec_c2VjcmV0LWtleS1ieXRlcy4uLg=="
}
```

Store `signing_secret` now. It is returned only by this create request and is omitted from later reads.

```bash theme={null}
curl -sS "https://api.relayapp.im/v1/webhook" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN"
```

```json theme={null}
{
  "id": "whk_01JZQ2Y0E9",
  "url": "https://agent.example.com/relay",
  "created_at": "2026-07-12T01:25:00.000Z"
}
```

Registering another webhook before deleting the active one returns `409 webhook_exists`.

## Verify a delivery

Relay follows Standard Webhooks and sends these headers:

```http theme={null}
webhook-id: evt_01JZE9M2XW
webhook-timestamp: 1783820103
webhook-signature: v1,BASE64_SIGNATURE
content-type: application/json
```

The signed bytes are:

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

The HMAC key is the base64-decoded portion after `whsec_`. Compute HMAC-SHA256, encode the result as base64, and compare it with the value after `v1,` using a timing-safe comparison. Reject timestamps more than five minutes from the current time.

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

export function verifyRelayWebhook(rawBody, headers, secret) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatureHeader = headers["webhook-signature"];

  if (!id || !timestamp || !signatureHeader) return false;

  const timestampSeconds = Number(timestamp);
  if (!Number.isFinite(timestampSeconds)) return false;
  if (Math.abs(Date.now() / 1000 - timestampSeconds) > 5 * 60) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const signed = `${id}.${timestamp}.${rawBody}`;
  const expected = crypto.createHmac("sha256", key).update(signed).digest();

  return signatureHeader.split(" ").some((versioned) => {
    const [version, encoded] = versioned.split(",", 2);
    if (version !== "v1" || !encoded) return false;

    let actual;
    try {
      actual = Buffer.from(encoded, "base64");
    } catch {
      return false;
    }

    return actual.length === expected.length &&
      crypto.timingSafeEqual(actual, expected);
  });
}
```

Pass the exact raw body bytes to verification. Parsing and re-serializing JSON before verification changes the signed content.

## Retries

A non-`2xx` response, network error, or 10-second delivery timeout is retried with exponential backoff and jitter. Relay makes up to 10 attempts. Since delivery is at least once and retries can be out of order, `event_id` deduplication is required.

## Delete or rotate

Delete the active webhook:

```bash theme={null}
curl -sS -X DELETE "https://api.relayapp.im/v1/webhook" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN"
```

```json theme={null}
{ "deleted": true }
```

Secret rotation without replacing the endpoint is **coming soon**. In v0, delete the webhook and register it again to receive a new one-time `signing_secret`. This creates a delivery gap, so coordinate the change with your consumer.

<Note>
  The v0 webhook delivers `message.received`, the only developer event currently emitted. Filtering by `event_types` and the rest of the SPEC event catalog are coming soon.
</Note>
