Chats

Chats

Manage iMessage and WhatsApp group chats and per-chat state via the /v1/chats/* endpoints.

All endpoints require a Bearer token (Authorization: Bearer tb_xxxxx). The chatId path parameter is the provider-assigned chat identifier returned from POST /v1/chats/groups and surfaced on chat.* webhook events. Chats are also persisted server-side as conversations; conversationId (a UUID) is returned alongside chatId on the create and get endpoints, and is the stable value used for sends keyed to an existing chat regardless of channel.

chatId format differs per channel. iMessage chats use iMessage;+;<guid>; WhatsApp groups use <id>@g.us. The conversationId UUID is the same shape on both.

Channel asymmetry. POST /:chatId/read and POST /:chatId/unread are iMessage-only — they return 400 CHANNEL_NOT_SUPPORTED on WhatsApp chats. Every other route works on both channels. See the WhatsApp Groups page for WhatsApp-specific quirks.

POST /v1/chats/groups — Create a group chat

Create a new group chat with one or more participants. Defaults to iMessage; pass channel: "whatsapp" to create a WhatsApp group.

Request

curl -X POST https://api.textbubbles.com/v1/chats/groups \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "participants": ["+14155551234", "+14155559876"],
    "name": "Launch Team"
  }'

Request Body

FieldTypeRequiredDescription
participantsstring[]YesOne or more phone numbers (E.164). On iMessage, Apple IDs are also accepted. On WhatsApp, values are normalized to JIDs server-side
namestringConditionalInitial display name. Required when channel is "whatsapp" (returns 400 NAME_REQUIRED if omitted). Optional for iMessage
channelstringNo"imessage" (default) or "whatsapp"
fromstringNoSender address (E.164). When supplied, the group is created from that specific instance; otherwise the customer’s default instance for the chosen channel is used

Response (201)

{
  "success": true,
  "data": {
    "chatId": "iMessage;+;chat1234567890",
    "conversationId": "550e8400-e29b-41d4-a716-446655440000",
    "channel": "imessage",
    "participants": ["+14155551234", "+14155559876"],
    "name": "Launch Team",
    "createdAt": "2026-04-21T10:00:00.000Z"
  },
  "requestId": "req_abc123"
}
FieldTypeDescription
chatIdstringProvider chat identifier. iMessage: iMessage;+;<guid>. WhatsApp: <id>@g.us
conversationIdstring | undefinedUUID of the persisted conversation row. Stable across channels. Omitted if persistence failed (the chat itself still exists)
channelstring"imessage" or "whatsapp". Present on WhatsApp responses; omitted on legacy iMessage responses
participantsstring[] | object[]iMessage: array of phone/Apple ID strings. WhatsApp: array of { handle, isPhoneResolved, admin } — see WhatsApp Groups
namestring | undefinedDisplay name (echoed name from the request, or what the provider stored)
createdAtstringISO-8601 creation timestamp

Creating a WhatsApp group

curl -X POST https://api.textbubbles.com/v1/chats/groups \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "whatsapp",
    "name": "Launch Team",
    "participants": ["+14155551234", "+14155559876"]
  }'

Notes for WhatsApp:

  • name is required by the WhatsApp protocol — omitting it returns 400 NAME_REQUIRED.
  • participants[] accepts E.164 phone numbers; the API normalizes each to a JID server-side.
  • Without from, the group is pinned to the customer’s default WhatsApp instance for the lifetime of the conversation. With from, the supplied instance is used.
  • Response chatId is the WhatsApp group JID (<id>@g.us). Use it on the other /v1/chats/* endpoints. The conversationId UUID is the stable identifier for sends via POST /v1/messages.

GET /v1/chats/:chatId — Get chat info

Retrieve metadata for a specific chat, including participants and the display name.

Request

curl https://api.textbubbles.com/v1/chats/iMessage%3B%2B%3Bchat1234567890 \
  -H "Authorization: Bearer YOUR_API_KEY"

URL-encode the chatId — it contains ; characters that some HTTP clients treat as path separators.

Response (200)

{
  "success": true,
  "data": {
    "chatId": "iMessage;+;chat1234567890",
    "conversationId": "550e8400-e29b-41d4-a716-446655440000",
    "participants": ["+14155551234", "+14155559876"],
    "name": "Launch Team",
    "isGroup": true
  },
  "requestId": "req_abc123"
}
FieldTypeDescription
chatIdstringProvider chat identifier (iMessage;+;<guid> or <id>@g.us)
conversationIdstring | undefinedUUID of the persisted conversation row
channelstring"imessage" or "whatsapp". Present on WhatsApp responses
participantsstring[] | object[]Current roster. iMessage: plain strings. WhatsApp: { handle, isPhoneResolved, admin } — see Participants below
namestring | undefinedDisplay name (groups only)
isGroupbooleantrue for groups, false for 1:1 chats

PUT /v1/chats/:chatId/name — Rename a group chat

Change the display name of an existing group chat. Only meaningful on group conversations.

Request

curl -X PUT https://api.textbubbles.com/v1/chats/iMessage%3B%2B%3Bchat1234567890/name \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Weekend plans" }'

Request Body

FieldTypeRequiredDescription
namestringYesNew display name (min length 1)

Response (200)

{
  "success": true,
  "data": { "chatId": "iMessage;+;chat1234567890", "name": "Weekend plans" },
  "requestId": "req_abc123"
}

The rename fires a chat.title.changed webhook event to participants — see Webhook Event Reference.

POST /v1/chats/:chatId/participants — Add a participant

Add a single participant to an existing group chat.

Request

curl -X POST https://api.textbubbles.com/v1/chats/iMessage%3B%2B%3Bchat1234567890/participants \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "participant": "+14155550100" }'

Request Body

FieldTypeRequiredDescription
participantstringYesPhone number (E.164) or Apple ID of the participant to add

Response (200)

{
  "success": true,
  "data": { "chatId": "iMessage;+;chat1234567890", "participant": "+14155550100" },
  "requestId": "req_abc123"
}

Emits chat.participant.added.

DELETE /v1/chats/:chatId/participants/:participantId — Remove a participant

Remove a participant from an existing group chat. participantId is the participant’s phone number or Apple ID.

Request

curl -X DELETE \
  "https://api.textbubbles.com/v1/chats/iMessage%3B%2B%3Bchat1234567890/participants/+14155550100" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

{
  "success": true,
  "data": {
    "chatId": "iMessage;+;chat1234567890",
    "removedParticipant": "+14155550100"
  },
  "requestId": "req_abc123"
}

Emits chat.participant.left.

POST /v1/chats/:chatId/leave — Leave a group chat

Leave a group chat. After this call your sender address will stop receiving messages from the chat.

Request

curl -X POST https://api.textbubbles.com/v1/chats/iMessage%3B%2B%3Bchat1234567890/leave \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

{
  "success": true,
  "data": { "chatId": "iMessage;+;chat1234567890", "left": true },
  "requestId": "req_abc123"
}

POST /v1/chats/:chatId/read — Mark a chat as read

Mark all messages in the chat as read. Sends read receipts to the other participants if receipts are enabled.

iMessage only. Returns 400 CHANNEL_NOT_SUPPORTED on WhatsApp chats — WhatsApp does not expose a per-chat read ack. Acknowledge reads per-message instead.

Request

curl -X POST https://api.textbubbles.com/v1/chats/iMessage%3B%2B%3Bchat1234567890/read \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

{
  "success": true,
  "data": { "chatId": "iMessage;+;chat1234567890", "read": true },
  "requestId": "req_abc123"
}

POST /v1/chats/:chatId/unread — Mark a chat as unread

Set the unread indicator on the chat. Requires macOS 13 or later on the sender’s instance.

iMessage only. Returns 400 CHANNEL_NOT_SUPPORTED on WhatsApp chats.

Request

curl -X POST https://api.textbubbles.com/v1/chats/iMessage%3B%2B%3Bchat1234567890/unread \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

{
  "success": true,
  "data": { "chatId": "iMessage;+;chat1234567890", "unread": true },
  "requestId": "req_abc123"
}

POST /v1/chats/:chatId/typing — Send a typing indicator

Start or stop showing a typing indicator in the chat. The indicator times out automatically after a short interval if stop is not sent.

Request

curl -X POST https://api.textbubbles.com/v1/chats/iMessage%3B%2B%3Bchat1234567890/typing \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "start" }'

Request Body

FieldTypeRequiredDescription
statusstringYes"start" to show the indicator, "stop" to hide it

Response (200)

{
  "success": true,
  "data": { "chatId": "iMessage;+;chat1234567890", "typing": true },
  "requestId": "req_abc123"
}

Works on both channels. On WhatsApp, "start" maps to the composing presence state and "stop" maps to paused.

Participants

WhatsApp group rosters surface each participant as an object rather than a bare string:

FieldTypeDescription
handlestringThe participant’s address as the provider exposed it. Typically a phone JID (<digits>@s.whatsapp.net); may be a Linked Device ID (<id>@lid) when the participant has not been phone-resolved yet
isPhoneResolvedbooleantrue when handle is a phone-based JID, false when it is a @lid. Treat this — not the format of handle — as the source of truth on whether the phone is known
adminstring | null"admin", "superadmin", or null

A participant may transition from isPhoneResolved: false to isPhoneResolved: true over the lifetime of the group as their phone number becomes known. The conversationId and the participant’s identity in your local store should be keyed off whichever identifier you receive first; subsequent webhooks reuse the same logical participant.

isPhoneResolved also appears in:

  • conversation.participants[] on message.inbound and outbound lifecycle webhook payloads attributed to a WhatsApp group.
  • participants[] returned by POST /v1/chats/groups and GET /v1/chats/:chatId for WhatsApp chats.

iMessage participants are returned as plain strings — there is no LID concept on iMessage.