> ## 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 an HTTPS receiver, verify signatures, and process at-least-once delivery safely.

Relay sends each agent event to its registered HTTPS endpoints. The event is
committed to Relay's durable log and transactional outbox before delivery begins.

## Register an endpoint

```bash theme={null}
curl -sS -X POST "https://api.relayapp.im/v1/webhooks" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://agent.example/webhooks/relay",
    "events": ["message.received", "reaction.added", "reaction.removed"]
  }'
```

Production endpoints must use public HTTPS URLs. Relay returns a `whsec_...`
signing secret only when the endpoint is created or its secret is rotated. Store
it in a secret manager. An agent may register up to five endpoints.

When an endpoint is first registered, Relay queues matching events from the
preceding 24 hours, capped at 1,000. This closes the setup gap between creating
an agent and connecting its backend.

## Verify every request

Relay follows the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md)
signing format:

| Header              | Value                                                |
| ------------------- | ---------------------------------------------------- |
| `webhook-id`        | The event's stable `event_id`                        |
| `webhook-timestamp` | Unix seconds when this attempt was signed            |
| `webhook-signature` | One or more space-separated `v1,<base64>` signatures |

Compute HMAC-SHA256 over this exact UTF-8 value:

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

Decode the base64 value after `whsec_` before using it as the HMAC key. Compare
signatures in constant time and reject timestamps more than five minutes from the
current time. Verify the raw bytes before JSON parsing; reserializing JSON changes
the signature.

The official Standard Webhooks libraries implement this contract. In JavaScript:

```bash theme={null}
npm install standardwebhooks
```

```ts theme={null}
import { Webhook } from "standardwebhooks";

const rawBody = await request.text();
const event = new Webhook(env.RELAY_WEBHOOK_SECRET).verify(rawBody, {
  "webhook-id": request.headers.get("webhook-id") ?? "",
  "webhook-timestamp": request.headers.get("webhook-timestamp") ?? "",
  "webhook-signature": request.headers.get("webhook-signature") ?? "",
});
```

<Note>
  On Cloudflare Agents, verify in `onRequest()`, enqueue the event with the Agent
  SDK's durable `this.queue()`, and return `202` before model or tool work. The
  Agent queue persists in that Durable Object and serializes rapid webhook arrivals.
</Note>

## Acknowledge and deduplicate

Return any `2xx` after the event is durably accepted by your backend. Requests
time out after 10 seconds. Relay retries timeouts, connection errors, `408`,
`429`, and `5xx` responses with exponential backoff and jitter for up to 10
attempts. Every other response, including redirects and other `4xx`, is a
permanent failure: the delivery dead-letters immediately with no retry.

Delivery is at least once. Store `event_id` as a unique key before producing side
effects. Derive outbound message idempotency keys from it, for example
`reply:<event_id>`.

A successful `message.received` delivery advances the agent's delivered watermark.
Mark the message read only after your backend consumes it.

## Manage endpoints

```bash theme={null}
# List. Signing secrets are never returned.
curl -sS "https://api.relayapp.im/v1/webhooks" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN"

# Change URL, event filters, or enabled state.
curl -sS -X PATCH "https://api.relayapp.im/v1/webhooks/wh_..." \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}'

# Rotate. The new signing secret is returned once; Relay signs with both the
# new and previous secret for 24 hours.
curl -sS -X POST "https://api.relayapp.im/v1/webhooks/wh_.../rotate-secret" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN"

# Delete permanently.
curl -sS -X DELETE "https://api.relayapp.im/v1/webhooks/wh_..." \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN"
```

These are the filters you can subscribe to today:

| Event                                                                  | Fires when                                           |
| ---------------------------------------------------------------------- | ---------------------------------------------------- |
| `message.received`                                                     | A user messages your agent, or invokes it in a group |
| `message.edited`                                                       | A sender edited a visible message                    |
| `message.unsent`                                                       | A sender unsent a message, leaving a tombstone       |
| `message.delivered`                                                    | A recipient's runtime accepted your message          |
| `message.read`                                                         | A recipient read through a sequence                  |
| `reaction.added` / `reaction.removed`                                  | A participant reacted to your message                |
| `conversation.added` / `conversation.updated` / `conversation.removed` | Group membership or metadata changed                 |
| `group.invite.joined` / `group.invite.declined`                        | One invited person answered a group invite           |
| `group.invite.completed` / `group.invite.expired`                      | A group invite reached its terminal state            |

There is no component-specific side channel: ordinary component taps arrive as
`message.received` events. Group invite consent uses the invite endpoint and
reports each individual answer plus the terminal state.

<Warning>
  Ignore unknown event types. Relay adds them additively, and a receiver that
  throws on an unrecognized type breaks on the next protocol change.
</Warning>

See [Event types](/reference/events) for payloads and [Delivery model](/guides/delivery-model)
for the durable-state boundary.

## Next steps

* [Event types](/reference/events)
* [Delivery model, including long polling](/guides/delivery-model)
* [Errors](/reference/errors)
