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

# Group conversations

> Receive explicit invocations in a group, reply to them, and follow membership changes.

People create groups in the Relay app and add agents to them. Your backend
receives an event only when a human explicitly invokes your agent, and replies
to that invocation with the ID Relay supplied.

## The invocation boundary

Group membership is not transcript authority. This is the single rule that
shapes everything else on this page.

| Your backend receives                      | Your backend does not receive        |
| ------------------------------------------ | ------------------------------------ |
| Messages that explicitly invoke your agent | Ambient group conversation           |
| Your agent's own replies, in history       | Messages between other participants  |
| Membership and metadata lifecycle events   | Any message from before it was added |

<Warning>
  An agent added to a group yesterday cannot read what the group said this
  morning. Only invocations reach you.
</Warning>

## Receive an invocation

A group `message.received` carries an extra `data.invocation_id` alongside the
usual message envelope.

```json theme={null}
{
  "event_type": "message.received",
  "agent_id": "agt_01JZRELAY",
  "data": {
    "invocation_id": "inv_01JZC7INVOKE",
    "message": {
      "id": "msg_01JZM3T8AH",
      "conversation_id": "cnv_01JZC7K4RQ",
      "sequence": 12,
      "sender": { "kind": "user", "id": "usr_01JZU1F0BD" },
      "parts": [{ "part_index": 0, "type": "text", "text": "@scheduler when are we free?" }],
      "status": "sent",
      "created_at": "2026-07-17T12:00:00.000Z"
    }
  }
}
```

Treat `invocation_id` as required state for the reply. Store it with the
`event_id` you are already deduplicating on.

## Reply to an invocation

Reply exactly as you would in a direct conversation, and pass the
`invocation_id` through.

```bash theme={null}
curl -sS -X POST "$RELAY_API_URL/v1/messages" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reply-evt_01JZE9M2XW" \
  -d '{
    "conversation_id": "cnv_01JZC7K4RQ",
    "invocation_id": "inv_01JZC7INVOKE",
    "parts": [{ "type": "text", "text": "Thursday after 4pm works for everyone." }]
  }'
```

A streaming reply carries it as a **query parameter**, not a body field:

```bash theme={null}
curl -sS -X POST \
  "$RELAY_API_URL/v1/messages?stream=true&conversation_id=cnv_01JZC7K4RQ&invocation_id=inv_01JZC7INVOKE" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN" \
  -H "Idempotency-Key: reply-evt_01JZE9M2XW" \
  -H "Content-Type: text/event-stream" \
  -H "x-vercel-ai-ui-message-stream: v1" \
  --data-binary @reply.sse
```

### Invocation errors

| Status                | Message                                                         | Cause                                                |
| --------------------- | --------------------------------------------------------------- | ---------------------------------------------------- |
| `403 forbidden`       | `group agent replies require invocation_id`                     | A group reply omitted it                             |
| `403 forbidden`       | `group agent streams require invocation_id`                     | A group stream omitted the query parameter           |
| `403 forbidden`       | `invocation is invalid, completed, or belongs to another agent` | The invocation was already consumed, or is not yours |
| `403 forbidden`       | `invocation belongs to an ended membership period`              | Your agent was removed and re-added since            |
| `403 forbidden`       | `message is outside this agent's invocation scope`              | The reply targets content you were never invoked on  |
| `422 invalid_request` | `invocation_id must be a Relay invocation id`                   | Malformed ID                                         |

<Warning>
  An invocation is consumed once. Reuse the same `Idempotency-Key` to retry a
  reply safely; do not reuse the `invocation_id` for a second, different message.
</Warning>

## Membership and metadata events

Relay emits lifecycle events to every active group agent.

| Event                  | Emitted when                                    |
| ---------------------- | ----------------------------------------------- |
| `conversation.added`   | A human adds your agent to a group              |
| `conversation.updated` | A human renames the group or changes its avatar |
| `conversation.removed` | A human removes your agent                      |

Each payload is typed and self-contained: `conversation_id`, the human `actor`,
the affected participant when the mutation targets one, the current
`membership_version`, a structured `system_mutation` with old and new values,
and the canonical system `message`.

Full payloads are in [event types](/reference/events).

<Warning>
  A lifecycle event grants no transcript access. `conversation.added` tells you
  that you are a member, not what the group has been saying.
</Warning>

## Propose a group

`POST /v1/groups/invites` commits one consent card into each target's direct
conversation with your agent. Every target must have your agent added, and each
person answers for themselves.

```bash theme={null}
curl -sS -X POST "$RELAY_API_URL/v1/groups/invites" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invite-founders-dinner" \
  -d '{
    "title": "Founders dinner",
    "user_ids": ["usr_01K1M8ALICE", "usr_01K1M8BOB"],
    "reason": "You both asked for an introduction"
  }'
```

The first accept creates the conversation and puts that person in it beside
your agent. Every later accept joins the same conversation right away, and the
invite stays open for whoever has not answered yet.

| Invite `state` | Meaning                                                 | Conversation               |
| -------------- | ------------------------------------------------------- | -------------------------- |
| `pending`      | Open, nobody has joined yet                             | Not created                |
| `active`       | Open, at least one person joined                        | Live, at `conversation_id` |
| `completed`    | Closed because everyone answered or the deadline passed | Live, at `conversation_id` |
| `expired`      | Closed with nobody joined                               | Never created              |

| Member `state` | Meaning                                        |
| -------------- | ---------------------------------------------- |
| `invited`      | Has not answered yet                           |
| `accepted`     | Joined the live conversation                   |
| `declined`     | Answered no, and stays out of the conversation |

Read one member's `state` to know whether that person is in the conversation.
A `conversation_id` on a `pending` invite names the destination group your
agent proposed into, so it tells you where the invite leads rather than who
consented.

| Event                    | Emitted when                                                |
| ------------------------ | ----------------------------------------------------------- |
| `group.invite.joined`    | One person accepts. Carries `conversation_id` and `user_id` |
| `group.invite.declined`  | One person declines. Carries `user_id`                      |
| `group.invite.completed` | The invite closes with a live conversation                  |
| `group.invite.expired`   | The invite closes with nobody joined                        |

<Note>
  A decline answers for that person alone. It removes nobody from a live
  conversation and leaves the invite open for everyone still deciding.
</Note>

## Limits

| Limit                           | Value                               |
| ------------------------------- | ----------------------------------- |
| Participants per group          | 25, including every human and agent |
| Historical participants tracked | 100                                 |
| Group title                     | 1 to 100 characters                 |
| Agents required per group       | At least 1                          |
| Invocation stream claim         | 5 minutes                           |

## What people do, and what backends cannot

Group creation and membership are first-party app actions, authenticated with a
person's Relay session. There is no Agent Token route for them.

| Action                                | Who                                                                         |
| ------------------------------------- | --------------------------------------------------------------------------- |
| Create a group, set its title         | A person, in the app                                                        |
| Add or remove a participant           | A person, in the app                                                        |
| Rename the group or change its avatar | A person, in the app                                                        |
| Leave the group                       | A person, in the app                                                        |
| Invoke an agent                       | A person, in the app                                                        |
| Reply to an invocation                | Your backend                                                                |
| Send a group invite card              | Your backend, via `POST /v1/groups/invites`; the person consents in the app |
| Accept or decline an invite           | A person, in the app                                                        |

<Note>
  Direct membership writes stay human. A backend can propose with an invite card,
  and the `group.invite.*` events report each answer and the terminal state, but
  only a person's consent changes who is in a group. Fuller agent-initiated
  management is on the [roadmap](/roadmap).
</Note>

## Next steps

* [Event types](/reference/events) for the exact lifecycle payloads
* [Sending messages](/guides/sending-messages) for every part type
* [Streaming replies](/guides/streaming)
* [Developer data access and retention](/reference/data-and-permissions)
