# Relay Webhooks

[Relay](https://docs.relayapp.im/) (relayapp.im) is a messenger where people chat with AI agents. An agent's
webhook subscription receives message, reaction, participant, chat, contact, call, payment, location and
rating events as a JSON `POST` signed with
Standard Webhooks headers: `webhook-id`,
`webhook-timestamp` and `webhook-signature`. The envelope is
`{api_version, webhook_version, event_type, event_id, created_at, trace_id, agent_id, data}`; the event name
is the top-level `event_type`, and `event_id` is the deduplication key.

## When to Use This Skill

* How do I receive Relay webhooks?
* How do I verify Relay webhook signatures (`webhook-signature`, `whsec_` secret)?
* How do I handle `message.received`, `message.sent` or `message.failed` events?
* How do I subscribe to `reaction.added`, `participant.added` or `chat.created`?
* Why is my Relay webhook signature verification failing, or rejecting Hookdeck retries?

## Verification (core)

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`. Relay strips the `whsec_` prefix,
base64-decodes the rest as the HMAC key, signs with HMAC-SHA256, base64-encodes the result, and sends one or
more space-separated `v1,<signature>` entries. Use a Standard Webhooks library (`standardwebhooks` on npm and
PyPI), which compares in constant time. Read the `webhook-*` headers, and when `webhook-id` is absent fall
back to the `svix-*` headers (`svix-id`, `svix-timestamp`, `svix-signature`), as Hookdeck does.

```javascript
const { Webhook } = require('standardwebhooks');

const prefix = req.get('webhook-id') ? 'webhook' : 'svix';
const headers = {
  'webhook-id': req.get(`${prefix}-id`),
  'webhook-timestamp': req.get(`${prefix}-timestamp`),
  'webhook-signature': req.get(`${prefix}-signature`),
};
// rawBody: the raw Buffer/string, never re-serialized JSON
const event = new Webhook(process.env.RELAY_MESSENGER_WEBHOOK_SECRET).verify(rawBody, headers);
// throws on a bad signature or a timestamp outside the window; returns the parsed body

```

```python
from standardwebhooks.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["RELAY_MESSENGER_WEBHOOK_SECRET"])  # "whsec_..."
event = wh.verify(raw_body, {"webhook-id": id_, "webhook-timestamp": ts, "webhook-signature": sig})

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/relay-messenger-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/relay-messenger-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/relay-messenger-webhooks/examples/fastapi/).

The secret is returned once as `signing_secret` when you create a webhook subscription
(`POST /v1/webhook-subscriptions`), and it can't be read again. It starts with `whsec_`. Keep the full value
in `RELAY_MESSENGER_WEBHOOK_SECRET`. Handlers fail closed: with the variable unset they return 500 instead of
accepting anything. A missing header or a bad signature returns 400. Relay treats a `401` (or any non-2xx/429/5xx
status) from your receiver as **terminal** and stops retrying that event, so only reject deliveries that are
really invalid. Relay's own TypeScript SDK also verifies with `relay.webhooks.unwrap(rawBody, { headers })`.

### Hookdeck retries and replays hit the 5-minute window

The Standard Webhooks libraries always enforce a fixed 5-minute timestamp window, and it can't be
configured. Hookdeck retries and replays resend the **original** `webhook-timestamp`, so the library
rejects any retry or replay that happens more than 5 minutes after the original delivery. If you verify
behind Hookdeck (or replay old events), use the manual verifier in
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/relay-messenger-webhooks/references/verification.md), where the window can be dropped or widened.
Deduplicate on `event_id` instead of relying on the timestamp. Relay's own retries reuse the event ID and body
under a new `webhook-timestamp` (and so a new signature), which is why `webhook-id` is a stable event ID.

## Common Event Types

27 events are documented (the list may not be exhaustive). The name is `event_type` in the JSON body, and only
`data` varies by type. The examples handle these:

| Event | Triggered When |
| --- | --- |
| `message.received` | A message from a person or another agent is committed for your agent |
| `message.sent` | A message your agent sent is committed |
| `message.delivered` | A message your agent sent reaches Delivered |
| `message.read` | A message your agent sent reaches Read |
| `message.failed` | A recipient agent's `message.received` webhook delivery went terminal |
| `reaction.added` | A contact adds a reaction to a message part |
| `reaction.removed` | A contact removes a reaction |
| `participant.added` | A contact joins a group chat |
| `participant.removed` | A contact leaves or is removed from a group chat |
| `chat.created` | Relay creates a chat that includes your agent |

Relay also sends `chat.group_name_updated`, `chat.group_icon_updated`, `chat.typing_indicator.started`/`.stopped`,
`contact.added`/`.removed`, `call.created`/`.updated`/`.ended`, `payment.succeeded`/`.canceled`/`.expired`,
`location.sharing.started`/`.stopped` and `rating.created`/`.updated`/`.deleted`. See
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/relay-messenger-webhooks/references/overview.md) for the envelope and delivery behavior.

## Environment Variables

```bash
# signing_secret of the webhook subscription (Standard Webhooks format, starts with whsec_)
RELAY_MESSENGER_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 relay-messenger --path /webhooks/relay-messenger

```

Create a webhook subscription whose `target_url` is the tunnel URL, then message your agent in Relay to see
an event arrive. Relay only delivers to public HTTPS targets and treats redirects as terminal, so register the
final URL.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/relay-messenger-webhooks/references/overview.md) - Relay webhook concepts, events, payload envelope, delivery and retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/relay-messenger-webhooks/references/setup.md) - Create a subscription through the API, store the secret, manage it
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/relay-messenger-webhooks/references/verification.md) - Standard Webhooks verification details, header fallback, manual verifier, timestamp window