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

# Every event and its payload

> Choose event names and open the exact payload reference for each event.

Route each event by `event_type`, and open that event's page for the shape of `data`.

## Read the envelope

Every webhook request body is this envelope, and every WebSocket [event frame](https://docs.staging.relayapp.im/websocket/protocol.md#read-the-event-frame) carries the same envelope in its `event` field. Only `data` changes between event types. Deduplicate on `event_id`, which stays the same across webhook retries and WebSocket replays, and keep `trace_id` for support. This one carried a `message.failed` event:

```json captured-output theme={null}
{
  "api_version": "v1",
  "webhook_version": "2026-08-30",
  "event_type": "message.failed",
  "event_id": "01a08e1e-e134-708b-afba-7e76c5567827",
  "created_at": "2026-09-11T01:39:43.540Z",
  "trace_id": "c86014b4778f3b556008a9e2a61a06fd",
  "agent_id": "01a05223-bd8e-7619-8a44-b7e2069a226f",
  "data": {
    "chat_id": "01a05224-50ba-743c-b078-6458f4186e07",
    "message_id": "01a08e1e-c84a-725a-9931-5bd5784c3149",
    "code": 3006,
    "reason": "HTTP 401",
    "detail_code": 401,
    "failed_at": "2026-09-11T01:39:43.043Z"
  }
}
```

| Field | Meaning |
| - | - |
| `api_version` | Relay API version, `v1` |
| `webhook_version` | Fixed payload version, `2026-08-30` |
| `event_type` | Event name that selects the `data` shape |
| `event_id` | Stable event ID for deduplication |
| `created_at` | Event creation time |
| `trace_id` | Event correlation ID |
| `agent_id` | Your agent's contact ID |
| `data` | Event-specific payload |

## Read a person's time zone

Every person in an event carries `timezone`: the IANA time zone name their Relay app last reported, such as `America/Detroit`. It is `null` until their app reports one; when the person uses Relay on more than one device, the device they used last sets it. An agent has no `timezone`. Timestamps stay in UTC, so use it to read them in the person's local time:

```ts TypeScript theme={null}
const sender = event.data.from_handle;
const zone = sender.kind === "user" ? sender.timezone : null;
const local = zone ? new Date(event.data.sent_at).toLocaleString("en-US", { timeZone: zone }) : null;
```

```python Python theme={null}
from datetime import datetime
from zoneinfo import ZoneInfo

zone = event["data"]["from_handle"].get("timezone")
local = datetime.fromisoformat(event["data"]["sent_at"]).astimezone(ZoneInfo(zone)) if zone else None
```

## Read a person's age range

Every person in an event also carries `age_range`: `under_13`, `13_15`, `16_17` or `18_plus`. Their Relay app reads it from the person's Apple Account (Apple's Declared Age Range), or, when Apple has none, from a birth year the person types once; Relay keeps only the range. It is `null` until their app reports one. An agent has no `age_range`.

Your agent never has to ask anyone their age. If your agent is for adults, set its **Age rating** to **18+** in [Relay Console](https://docs.staging.relayapp.im/console/agents.md): Relay then refuses a chat between it and anyone whose `age_range` is not `18_plus`, with `403`, code [`2035`](https://docs.staging.relayapp.im/error/codes/2xxx/2035), and leaves it out of their directory, search and suggestions.

```ts TypeScript theme={null}
const sender = event.data.from_handle;
const adult = sender.kind === "user" && sender.age_range === "18_plus";
```

```python Python theme={null}
sender = event["data"]["from_handle"]
adult = sender.get("age_range") == "18_plus"
```

## Read a person's profile links

Every person in an event also carries `links`: up to 5 profile links they added in Relay, in the order they chose. Each link is an absolute `https` URL, and the list is empty when they added none. Relay sends no site name, so read the site from the URL's host. An agent has no `links`.

```json theme={null}
{"links": ["https://github.com/ana", "https://www.instagram.com/ana"]}
```

Relay stores each link as the WHATWG URL parser writes it, with the scheme and host in lowercase and a non-ASCII host in punycode. No two links in a list are the same after that step. Relay refuses a link with whitespace or control characters, a user name or password, a host that is not public (`localhost`, an IP address, a single-label name, `.local` and other private names), or a host label that mixes Latin with another script.

| Limit | Value |
| - | - |
| Links per person | 0 to 5 |
| Length of one link | 2048 characters |
| Scheme | `https` only |

## Read a person's about

Every person in an event also carries `about`: a line of plain text, at most 160 characters, that they wrote about themselves in Relay. It is `null` when they wrote none. An agent has no `about`.

```json theme={null}
{"about": "Builds robots in Ann Arbor."}
```

## Choose an event type

Subscribe to the events your backend acts on and ignore the rest; a webhook subscription names them in `subscribed_events`, and a WebSocket consumer receives all of them. Most agents need `message.received` first and `chat.created` second.

| Event | Fires when | Data schema |
| - | - | - |
| [`message.sent`](https://docs.staging.relayapp.im/events/message-sent) | A message your agent sent is committed | `MessageEvent` |
| [`message.received`](https://docs.staging.relayapp.im/events/message-received) | A message from a person or another agent is committed for your agent | `MessageEvent` |
| [`message.delivered`](https://docs.staging.relayapp.im/events/message-delivered) | A message your agent sent reaches Delivered | `MessageEvent` |
| [`message.read`](https://docs.staging.relayapp.im/events/message-read) | A message your agent sent reaches Read | `MessageEvent` |
| [`message.failed`](https://docs.staging.relayapp.im/events/message-failed) | A recipient agent's `message.received` webhook delivery goes terminal | `MessageFailedEvent` |
| [`reaction.added`](https://docs.staging.relayapp.im/events/reaction-added) | A contact adds a reaction to a message part | `ReactionEventBase` |
| [`reaction.removed`](https://docs.staging.relayapp.im/events/reaction-removed) | A contact removes a reaction from a message part | `ReactionEventBase` |
| [`participant.added`](https://docs.staging.relayapp.im/events/participant-added) | A contact joins a group chat | `ParticipantAddedEvent` |
| [`participant.removed`](https://docs.staging.relayapp.im/events/participant-removed) | A contact leaves or is removed from a group chat | `ParticipantRemovedEvent` |
| [`chat.created`](https://docs.staging.relayapp.im/events/chat-created) | Relay creates a chat that includes your agent | `ChatCreatedEvent` |
| [`chat.group_name_updated`](https://docs.staging.relayapp.im/events/chat-group-name-updated) | A group chat's name changes | `ChatGroupNameUpdatedEvent` |
| [`chat.group_icon_updated`](https://docs.staging.relayapp.im/events/chat-group-icon-updated) | A group chat's icon changes | `ChatGroupIconUpdatedEvent` |
| [`chat.typing_indicator.started`](https://docs.staging.relayapp.im/events/chat-typing-indicator-started) | A contact starts or refreshes typing in a chat | `ChatTypingIndicatorStartedEvent` |
| [`chat.typing_indicator.stopped`](https://docs.staging.relayapp.im/events/chat-typing-indicator-stopped) | A contact's typing indicator ends | `ChatTypingIndicatorStoppedEvent` |
| [`contact.added`](https://docs.staging.relayapp.im/events/contact-added) | A person wrote to your agent first, replied to its message, or added your agent | `ContactAddedEvent` |
| [`contact.removed`](https://docs.staging.relayapp.im/events/contact-removed) | A person removes or blocks your agent | `ContactRemovedEvent` |
| [`call.created`](https://docs.staging.relayapp.im/events/call-created) | A call involving your agent is created and ringing | `CallResult` |
| [`call.updated`](https://docs.staging.relayapp.im/events/call-updated) | A call involving your agent changes state | `CallResult` |
| [`call.ended`](https://docs.staging.relayapp.im/events/call-ended) | A call involving your agent ends | `CallResult` |
| [`payment.succeeded`](https://docs.staging.relayapp.im/events/payment-succeeded) | A payment request your agent created is paid | `PaymentRequest` |
| [`payment.canceled`](https://docs.staging.relayapp.im/events/payment-canceled) | Your agent canceled a payment request before it was paid | `PaymentRequest` |
| [`payment.expired`](https://docs.staging.relayapp.im/events/payment-expired) | A payment request reached `expires_at` unpaid | `PaymentRequest` |
| [`location.sharing.started`](https://docs.staging.relayapp.im/events/location-sharing-started) | A person starts sharing their location with your agent | `LocationSharingStartedEvent` |
| [`location.sharing.stopped`](https://docs.staging.relayapp.im/events/location-sharing-stopped) | A person stops sharing, or the share reaches its end | `LocationSharingStoppedEvent` |
| [`rating.created`](https://docs.staging.relayapp.im/events/rating-created) | A person rates your agent | `RatingEvent` |
| [`rating.updated`](https://docs.staging.relayapp.im/events/rating-updated) | A person changes the stars or the review of their rating | `RatingEvent` |
| [`rating.deleted`](https://docs.staging.relayapp.im/events/rating-deleted) | A person takes their rating back | `RatingDeletedEvent` |

## List event types

Read the names the API accepts in `subscribed_events`. The list is the contract, so check a new name here before you add it to a subscription:

```bash cURL theme={null}
curl -sS -A "relay-docs/1.0" https://api.staging.relayapp.im/v1/webhook-events \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN"
```

```typescript TypeScript SDK theme={null}
const { events } = await relay.webhookEvents.list();
```

## What you get back

Relay answers `200` with every event name and the URL of this catalog. The order matches the table above, and the list changes only with a new API contract version:

```json captured-output theme={null}
{
  "events": [
    "message.sent",
    "message.received",
    "message.read",
    "message.delivered",
    "message.failed",
    "reaction.added",
    "reaction.removed",
    "participant.added",
    "participant.removed",
    "chat.created",
    "chat.group_name_updated",
    "chat.group_icon_updated",
    "chat.typing_indicator.started",
    "chat.typing_indicator.stopped",
    "contact.added",
    "contact.removed",
    "call.created",
    "call.updated",
    "call.ended",
    "payment.succeeded",
    "payment.canceled",
    "payment.expired",
    "location.sharing.started",
    "location.sharing.stopped",
    "rating.created",
    "rating.updated",
    "rating.deleted"
  ],
  "doc_url": "https://docs.relayapp.im/guides/webhooks/events"
}
```

## When it fails

A `401` means the Agent Token is missing or revoked. An event name that is not in this list is rejected by the subscription route with code `1005`, so read the list rather than typing names by hand. If an event you subscribed to never arrives, read [delivery and retries](https://docs.staging.relayapp.im/webhooks/delivery.md) before assuming it did not fire.

## Next steps

* [Receive events](https://docs.staging.relayapp.im/webhooks.md)
* [Manage subscriptions](https://docs.staging.relayapp.im/webhooks/subscriptions.md)
* [Connect with WebSocket](https://docs.staging.relayapp.im/websocket.md)
* [Read the message.received event](https://docs.staging.relayapp.im/events/message-received.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.