Inkbox

> # Documentation index
> Fetch the complete documentation index at: https://inkbox.ai/sitemap.xml
> Use this file to discover all available pages before exploring further.

---

# Group chats
description: Start, continue, and read iMessage group chats from a dedicated outbound number

---


# Group chats

iMessage group chats use the existing [messages](/docs/api/imessage/messages) and [conversations](/docs/api/imessage/conversations) endpoints. They are available only to identities with an active attached `dedicated_outbound` iMessage number. Shared and dedicated inbound numbers remain 1:1.

This is separate from [group SMS/MMS](/docs/api/phone/texts): iMessage groups use iMessage conversation IDs and iMessage delivery state.

## Start or select a group `POST`


Pass 2-8 distinct E.164 phone numbers in `to`. An admin API key also passes the sending identity as `agent_identity_id`; an [identity-scoped API key](/docs/api-keys) always sends as its own identity.

```bash
curl -X POST "https://inkbox.ai/api/v1/imessage/messages?agent_identity_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890" \\
    -H "X-API-Key: YOUR_API_KEY" \\
    -H "Content-Type: application/json" \\
    -d '{
      "to": ["+14155550100", "+14155550101"],
      "text": "Planning starts at 10.",
      "send_style": "confetti"
    }'
```

The server normalizes the requested member set before resolving the conversation:

- One unresolved local group with the same requested members is reused before a remote thread has been established, whether creation is still pending or being retried.
- One established group with the same best-known members is reused only when its current membership can be verified and still matches the request.
- If no reusable match remains, a new group conversation is created.
- If multiple groups match, the request returns `409 ambiguous_group_conversation` instead of choosing one. Continue a specific thread by its `conversation_id`.

A `to` request never adds or removes members from an existing group. A different member set selects another exact match or creates a separate group. A scalar `to` value and a one-item `to` list keep their existing 1:1 behavior. `to` and `conversation_id` are mutually exclusive.

The `201` response uses the regular [Message object](/docs/api/imessage/messages#message-object). Group-specific fields look like this:

```json
{
    "message": {
      "id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
      "conversation_id": "82cf24f6-78fe-48da-a673-6a75b4f4a819",
      "assignment_id": null,
      "direction": "outbound",
      "remote_number": null,
      "sender_number": null,
      "participants": ["+14155550100", "+14155550101"],
      "is_group": true,
      "content": "Planning starts at 10.",
      "send_style": "confetti",
      "status": "pending",
      "recipients": [
        {
          "remote_number": "+14155550100",
          "delivery_status": "pending"
        },
        {
          "remote_number": "+14155550101",
          "delivery_status": "pending"
        }
      ]
    }
  }
```

Each previously unknown participant counts toward the dedicated outbound line's limit of 40 new contacts per rolling 24 hours. The whole group request is rejected if the line does not have enough remaining slots.

### Creation lifecycle

Group creation is asynchronous. The initial `201` means Inkbox stored the local conversation and queued the first message; it does not mean the remote thread is ready yet. Read the conversation or opt groups into the conversation list to inspect `group_creation_status`:

| Value | Meaning |
| :--- | :--- |
| `creating` | The initial group creation attempt is still resolving |
| `not_created` | No group has been created yet, or the latest creation attempt failed definitively |
| `ready` | The group is ready and later sends use the same remote thread |

While the conversation is `creating`, another send to the same `conversation_id` or exact `to` set is stored as another pending message on that local conversation and waits behind the elected creation send instead of initiating creation independently. If the initial attempt fails, the local conversation keeps the same `conversation_id` and the failed message remains visible in its history with `status: "error"`. Send the next message with that `conversation_id` to retry creation. A successful retry changes the conversation to `ready`; later sends continue the newly bound group without creating another local conversation.

## Continue the same group

Keep the `conversation_id` from the first response and use it for every later send into that exact thread:

```json
{
    "conversation_id": "82cf24f6-78fe-48da-a673-6a75b4f4a819",
    "text": "I moved the start time to 10:30.",
    "send_style": "gentle"
  }
```

`conversation_id` is the canonical group key. Do not reconstruct a thread from its `participants` when you need to continue a specific conversation: membership can change, and more than one real group can have the same members. A send by `conversation_id` continues that native thread with its verified current membership; all current recipients must pass the send preflight.

Group creation and conversation replies support the same 13 expressive [`send_style` values](/docs/api/imessage/messages#send-styles) as 1:1 messages. A style can accompany text, one `media_urls` item, or both:

```json
{
    "conversation_id": "82cf24f6-78fe-48da-a673-6a75b4f4a819",
    "text": "Here is the updated plan.",
    "media_urls": ["https://media.example.com/group-plan.png"],
    "send_style": "celebration"
  }
```

## List and read groups

Existing list endpoints exclude groups by default so current 1:1 callers keep the same result set and pagination. Pass `include_groups=true` to opt in:

```
GET /conversations?include_groups=true
GET /messages?include_groups=true
GET /conversation-projections?include_groups=true
```

Known conversation IDs work without the list flag:

```
GET /conversations/{conversation_id}
GET /messages?conversation_id={conversation_id}
GET /conversation-projections/{conversation_id}
```

REST conversation reads return `is_group: true`, `participants`, `group_creation_status`, and null `assignment_id`, `assignment_status`, and `remote_number` fields. Projection and MCP conversation reads expose `is_group`, `participants`, and the same null assignment/recipient fields, but do not include `group_creation_status`. Group messages return a participant snapshot. On inbound group messages, `sender_number` identifies who sent the message and `remote_number` mirrors that value for compatibility.

## Membership semantics

Conversation `participants` is an append-only, best-known history of E.164 phone numbers observed in the thread. A newly added member may not appear until later group activity, and a member who leaves is not removed from the stored list. Do not use this field as an authoritative current-membership or authorization check. Before an existing-group send or tapback is accepted, Inkbox resolves the thread's current recipients and applies opt-out and contact-rule checks to each one.

There is no API operation to add or remove a group member in V1. Membership changes happen in the native group conversation, and Inkbox updates its best-known participant history as activity reveals changes. After that history gains a member, sending to the original subset can create a separate group instead of continuing this one; use `conversation_id` whenever thread continuity matters.

## Delivery and current limitations

Outbound group messages include one `recipients` entry per participant. Group delivery transitions are message-level in V1, so the aggregate state is applied to those participant entries together. See [iMessage webhooks](/docs/api/imessage/webhooks#delivery-lifecycle-events).

Ready groups support [tapbacks](/docs/api/imessage/reactions) with the same targeting, replacement, and removal semantics as 1:1 conversations. Group reactions have `assignment_id: null`, and an inbound group reaction's `remote_number` identifies the participant who reacted.

Read receipts and typing indicators return `409 operation_not_supported_for_group_conversation`. MCP can opt group conversations into reads, but MCP group sends and actions are not available yet.

Detaching or reassigning the dedicated line leaves the old conversation readable as history to its original organization and identity, but sends and actions on it return `409`. A later line owner does not inherit the old group.

### Error responses

| Status | Description |
| :--- | :--- |
| 400 | The sending identity is not enabled for iMessage, or an admin send by `to` omitted `agent_identity_id` |
| 402 | The organization's current billing-cycle iMessage capacity is exhausted |
| 403 | At least one participant is blocked or opted out |
| 404 | The sending identity or requested `conversation_id` was not found or is not visible to the caller |
| 409 | The identity has no active dedicated outbound iMessage number, multiple groups match the member set, current membership cannot be resolved consistently or changes while the request is resolving, the line attachment changed, or the requested group action is not supported |
| 422 | A `to` list has more than 8 E.164 recipients, contains a duplicate or the sending line, or the body is otherwise invalid. A one-item list keeps 1:1 behavior |
| 429 | The identity's message limit or the line's new-contact limit was reached; honor `Retry-After` |
| 503 | `group_membership_unavailable` because current membership for an existing group could not be verified; retry with backoff |

Use the response's `detail.error` to choose the recovery path. Select a specific `conversation_id` for `ambiguous_group_conversation`. For `group_membership_changed`, verify the intended members and repeat the request so matching runs again. Retry `group_conversation_not_ready` only after the conversation is `ready` on its current line. Membership-consistency conflicts and `group_membership_unavailable` may be retried with backoff, while attachment and unsupported-operation conflicts require changing the line or operation instead of retrying unchanged.
