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). ThechatIdpath parameter is the provider-assigned chat identifier returned fromPOST /v1/chats/groupsand surfaced onchat.*webhook events. Chats are also persisted server-side as conversations;conversationId(a UUID) is returned alongsidechatIdon the create and get endpoints, and is the stable value used for sends keyed to an existing chat regardless of channel.
chatIdformat differs per channel. iMessage chats useiMessage;+;<guid>; WhatsApp groups use<id>@g.us. TheconversationIdUUID is the same shape on both.Channel asymmetry.
POST /:chatId/readandPOST /:chatId/unreadare iMessage-only — they return400 CHANNEL_NOT_SUPPORTEDon 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
| Field | Type | Required | Description |
|---|---|---|---|
participants | string[] | Yes | One or more phone numbers (E.164). On iMessage, Apple IDs are also accepted. On WhatsApp, values are normalized to JIDs server-side |
name | string | Conditional | Initial display name. Required when channel is "whatsapp" (returns 400 NAME_REQUIRED if omitted). Optional for iMessage |
channel | string | No | "imessage" (default) or "whatsapp" |
from | string | No | Sender 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"
}| Field | Type | Description |
|---|---|---|
chatId | string | Provider chat identifier. iMessage: iMessage;+;<guid>. WhatsApp: <id>@g.us |
conversationId | string | undefined | UUID of the persisted conversation row. Stable across channels. Omitted if persistence failed (the chat itself still exists) |
channel | string | "imessage" or "whatsapp". Present on WhatsApp responses; omitted on legacy iMessage responses |
participants | string[] | object[] | iMessage: array of phone/Apple ID strings. WhatsApp: array of { handle, isPhoneResolved, admin } — see WhatsApp Groups |
name | string | undefined | Display name (echoed name from the request, or what the provider stored) |
createdAt | string | ISO-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:
nameis required by the WhatsApp protocol — omitting it returns400 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. Withfrom, the supplied instance is used. - Response
chatIdis the WhatsApp group JID (<id>@g.us). Use it on the other/v1/chats/*endpoints. TheconversationIdUUID is the stable identifier for sends viaPOST /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"
}| Field | Type | Description |
|---|---|---|
chatId | string | Provider chat identifier (iMessage;+;<guid> or <id>@g.us) |
conversationId | string | undefined | UUID of the persisted conversation row |
channel | string | "imessage" or "whatsapp". Present on WhatsApp responses |
participants | string[] | object[] | Current roster. iMessage: plain strings. WhatsApp: { handle, isPhoneResolved, admin } — see Participants below |
name | string | undefined | Display name (groups only) |
isGroup | boolean | true 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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | New 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
| Field | Type | Required | Description |
|---|---|---|---|
participant | string | Yes | Phone 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_SUPPORTEDon 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_SUPPORTEDon 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
| Field | Type | Required | Description |
|---|---|---|---|
status | string | Yes | "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:
| Field | Type | Description |
|---|---|---|
handle | string | The 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 |
isPhoneResolved | boolean | true 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 |
admin | string | 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[]onmessage.inboundand outbound lifecycle webhook payloads attributed to a WhatsApp group.participants[]returned byPOST /v1/chats/groupsandGET /v1/chats/:chatIdfor WhatsApp chats.
iMessage participants are returned as plain strings — there is no LID concept on iMessage.