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

# Event types

> Event envelopes and payloads emitted by Relay.

Every event uses the same envelope:

```json theme={null}
{
  "event_id": "evt_01JZE9M2XW",
  "event_type": "message.received",
  "agent_id": "agt_01JZRELAY",
  "created_at": "2026-07-12T01:21:03.000Z",
  "data": { }
}
```

`event_id` is globally unique and serves as the deduplication key. Delivery is at least once. Ignore unknown `event_type` values so new types do not break the consumer.

See [Delivery model](/guides/delivery-model) for webhook acknowledgement,
idempotent writes, live state, and recovery.

## Emitted in v0

### `message.received`

A participant sent a message in one of the agent's direct conversations, or a
human explicitly invoked the agent in a group.

| Field                | Contents                                                                                                                                       |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `data.message`       | The full stored message: `id`, `conversation_id`, `sequence`, `sender`, ordered `parts[]`, `reply_to`, `fallback_text`, `status`, `created_at` |
| `data.invocation_id` | Group deliveries only. Required when replying, see [group conversations](/guides/group-conversations)                                          |

The agent's own messages never echo back as `message.received`.

A [message component](/components) tap is one of these ordinary messages. Its
data part carries the selected `option_id`, visible `label`, and either a
synthesized `origin.kind: "data_action"` object or the option's original origin.

### `message.edited`

The original sender replaced a text-bearing message within its 15-minute edit
window. `data.message` is the full updated canonical message. `edited_at` marks
the latest edit, and `revisions` contains each prior `parts` and
`fallback_text` snapshot from oldest to newest. `data.revision_count` is the
number of stored revisions, from 1 through 5.

```json theme={null}
{
  "event_type": "message.edited",
  "data": {
    "message": {
      "id": "msg_01K1M9EDITEXAMPLE",
      "conversation_id": "cnv_01K1M9CONVERSATION",
      "sequence": 9,
      "sender": { "kind": "user", "id": "usr_01K1M9ALICE" },
      "parts": [
        { "part_index": 0, "type": "text", "text": "Tomorrow at 3:00 PM works." }
      ],
      "reply_to": null,
      "reactions": [],
      "fallback_text": "Tomorrow at 3:00 PM works.",
      "status": "sent",
      "edited_at": "2026-07-24T20:15:00.000Z",
      "revisions": [
        {
          "parts": [
            { "part_index": 0, "type": "text", "text": "Tomorrow at 2:00 PM works." }
          ],
          "fallback_text": "Tomorrow at 2:00 PM works.",
          "replaced_at": "2026-07-24T20:15:00.000Z"
        }
      ],
      "created_at": "2026-07-24T20:05:00.000Z"
    },
    "revision_count": 1
  }
}
```

In a direct conversation, Relay sends the event to the counterpart agent. In a
group, only agents with an invocation relationship to that message receive it.
Edits do not produce push notifications or change message sequence.

### `message.unsent`

The original sender unsent a message within two minutes of creation. The event
identifies the stable tombstone:

```json theme={null}
{
  "event_type": "message.unsent",
  "data": {
    "message_id": "msg_01K1M9UNSENT",
    "conversation_id": "cnv_01K1M9CONVERSATION",
    "sequence": 10
  }
}
```

History keeps the id, sequence, sender, status `deleted`, and original
`created_at`, but omits parts and reactions. Unsend uses the same direct and
group agent scope as `message.edited` and does not produce a push notification.

### `message.delivered`

A recipient's runtime accepted the agent's message. Everything through `through_sequence` is delivered to `participant`.

```json theme={null}
{
  "event_type": "message.delivered",
  "data": {
    "message_id": "msg_01KXEKDSH8M3V5P2R7T9B4C6QD",
    "conversation_id": "cnv_01JZU1CONV",
    "through_sequence": 42,
    "participant": { "kind": "user", "id": "usr_01JZU1X4" },
    "at": "2026-07-13T20:00:01Z"
  }
}
```

### `message.read`

The payload matches `message.delivered`. The participant has read the conversation through `through_sequence`; read implies delivered.

To advance the agent's own read watermark, see [Read receipts](/guides/read-receipts).

### `reaction.added` / `reaction.removed`

A participant added or removed a reaction from the agent's message.

```json theme={null}
{
  "event_type": "reaction.added",
  "data": {
    "reaction": {
      "message_id": "msg_01JZM4Q9VN",
      "part_index": null,
      "type": "emoji",
      "emoji": "🔥",
      "actor": { "kind": "user", "id": "usr_01JZU1F0BD" },
      "operation": "add"
    }
  }
}
```

`part_index` is `null` for whole-message reactions. `emoji` is present if and only if
`type` is `"emoji"`.

### `conversation.added`, `conversation.updated`, and `conversation.removed`

Relay emits these when a human group member adds or removes the authenticated
agent. Membership alone does not disclose ambient messages. Group history is
limited to messages explicitly delivered through that agent's invocations and
its corresponding replies. Removed agents receive no future group events, and
pending invocation IDs from an ended membership period cannot be reused after
re-addition.

Relay emits `conversation.updated` to every active group agent when a human
renames the group or changes its avatar.

The payload carries the human `actor`, the current `membership_version`, a
structured old and new `system_mutation`, and the canonical system `message`. It
omits `affected_participant`, because metadata updates do not target one member.

<Warning>
  No lifecycle event grants ambient transcript access.
</Warning>

Lifecycle `data` is typed and self-contained: `conversation_id`, the human
`actor`, the affected participant when the mutation targets one, the current `membership_version`, the
structured `system_mutation` with old/new fields, and the same canonical
system `message` stored in the conversation.

<CodeGroup>
  ```json Membership change theme={null}
  {
    "conversation_id": "cnv_01JZC7K4RQ",
    "actor": { "kind": "user", "id": "usr_01JZU1F0BD" },
    "affected_participant": { "kind": "agent", "id": "agt_01JZRELAY" },
    "membership_version": 2,
    "system_mutation": {
      "type": "group.mutation",
      "mutation": "membership.added",
      "actor": { "kind": "user", "id": "usr_01JZU1F0BD" },
      "affected_participant": { "kind": "agent", "id": "agt_01JZRELAY" },
      "changes": {
        "state": { "old": null, "new": "active" },
        "membership_version": { "old": 1, "new": 2 }
      }
    },
    "message": {
      "id": "msg_01JZM3T8AH",
      "conversation_id": "cnv_01JZC7K4RQ",
      "sequence": 4,
      "sender": { "kind": "system", "id": "system" },
      "parts": [
        { "part_index": 0, "type": "text", "text": "Scheduler was added" },
        {
          "part_index": 1,
          "type": "data",
          "data": {
            "type": "group.mutation",
            "mutation": "membership.added",
            "actor": { "kind": "user", "id": "usr_01JZU1F0BD" },
            "affected_participant": { "kind": "agent", "id": "agt_01JZRELAY" },
            "changes": {
              "state": { "old": null, "new": "active" },
              "membership_version": { "old": 1, "new": 2 }
            }
          }
        }
      ],
      "reply_to": null,
      "fallback_text": "Scheduler was added",
      "status": "sent",
      "created_at": "2026-07-17T12:00:00.000Z"
    }
  }
  ```

  ```json Metadata change theme={null}
  {
    "conversation_id": "cnv_01JZC7K4RQ",
    "actor": { "kind": "user", "id": "usr_01JZU1F0BD" },
    "membership_version": 2,
    "system_mutation": {
      "type": "group.mutation",
      "mutation": "metadata.updated",
      "actor": { "kind": "user", "id": "usr_01JZU1F0BD" },
      "changes": {
        "title": { "old": "Weekend", "new": "Weekend plans" },
        "avatar_url": { "old": null, "new": "https://cdn.relayapp.im/group.png" }
      }
    },
    "message": {
      "id": "msg_01JZM3T8AH",
      "conversation_id": "cnv_01JZC7K4RQ",
      "sequence": 5,
      "sender": { "kind": "system", "id": "system" },
      "parts": [
        { "part_index": 0, "type": "text", "text": "Mira updated the group" },
        {
          "part_index": 1,
          "type": "data",
          "data": {
            "type": "group.mutation",
            "mutation": "metadata.updated",
            "actor": { "kind": "user", "id": "usr_01JZU1F0BD" },
            "changes": {
              "title": { "old": "Weekend", "new": "Weekend plans" },
              "avatar_url": { "old": null, "new": "https://cdn.relayapp.im/group.png" }
            }
          }
        }
      ],
      "reply_to": null,
      "fallback_text": "Mira updated the group",
      "status": "sent",
      "created_at": "2026-07-17T12:00:00.000Z"
    }
  }
  ```
</CodeGroup>

Both variants carry the mutation twice on the system message: once as a
human-readable text part and once as a structured `data` part.

People manage group membership and metadata from the Relay app. A backend
receives the lifecycle events above but cannot create a group or change its
membership; see [API availability](/roadmap).

### `group.invite.joined` / `group.invite.declined`

Each invited user answers individually. `group.invite.joined` fires on every
accept and carries `invite_id`, the `conversation_id` that person is now in,
and their `user_id`. The first accept creates that conversation, and later
accepts join it. `group.invite.declined` carries `invite_id` and `user_id`.

```json theme={null}
{
  "event_type": "group.invite.joined",
  "data": {
    "invite_id": "inv_01K1M8FOUNDERSDINNER",
    "conversation_id": "cnv_01K1M8NEWGROUP",
    "user_id": "usr_01K1M8ALICE"
  }
}
```

### `group.invite.completed` / `group.invite.expired`

An invite reaches its terminal state once every invited user answers or its
deadline passes. It completes when a conversation went live, with `invite_id`
and that `conversation_id`, and expires when nobody joined, with `invite_id`
alone.

```json theme={null}
{
  "event_type": "group.invite.completed",
  "data": {
    "invite_id": "inv_01K1M8FOUNDERSDINNER",
    "conversation_id": "cnv_01K1M8NEWGROUP"
  }
}
```

A decline closes that person's card alone. It leaves the conversation and the
rest of the invite untouched.

Group mutations are an off-by-default preview. Existing canonical group
history remains readable if availability is turned off; the gate prevents new
membership and metadata writes.

## Specified, coming soon

The Relay contract also defines `message.failed`, durable typing events,
`attachment.available`, install events, and the `call.*` family. These are
**not emitted by v0**. See [API availability](/roadmap).

## See also

* [Webhooks](/guides/webhooks)
* [Delivery model](/guides/delivery-model)
* [API availability](/roadmap)
