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). Responses return a canonicalchatobject — see The chat fragment for the field reference. ThechatIdpath parameter on these endpoints is the provider-assigned chat identifier surfaced onchat.*webhook events.Path
chatIdformat differs per channel. iMessage chats useiMessage;+;<guid>; WhatsApp groups use<id>@g.us. Thechat.chatIdUUID returned in responses is the channel-independent identifier — use it aschatIdonPOST /v1/messagesto send into a chat.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.
The chat fragment
Every /v1/chats/* response embeds data.chat, a single canonical object that also appears on message.* and chat.* webhook events:
| Field | Type | Description |
|---|---|---|
chatId | string (UUID) | Stable identifier for the conversation. The same value across channels. Pass this as chatId on POST /v1/messages to send into the chat |
providerChatGuid | string | null | Provider chat identifier — iMessage;+;<guid> or <id>@g.us. The path-parameter chatId on /v1/chats/:chatId/* endpoints |
channel | string | imessage, sms, or whatsapp |
isGroup | boolean | true for group chats, false for 1:1 |
name | string | null | Group display name. null for 1:1 chats |
participants | object[] | Active roster. Each entry carries handle, contactId, role, and isPhoneResolved. role is member, owner, or self (the customer’s own instance). isPhoneResolved is true when handle is a phone-based identifier and false when only a WhatsApp Linked Device ID (@lid) is known |
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": {
"chat": {
"chatId": "550e8400-e29b-41d4-a716-446655440000",
"providerChatGuid": "iMessage;+;chat1234567890",
"channel": "imessage",
"isGroup": true,
"name": "Launch Team",
"participants": [
{ "handle": "+14155551234", "contactId": null, "role": "member", "isPhoneResolved": true },
{ "handle": "+14155559876", "contactId": null, "role": "member", "isPhoneResolved": true },
{ "handle": "self", "contactId": null, "role": "self", "isPhoneResolved": true }
]
},
"createdAt": "2026-04-21T10:00:00.000Z"
},
"requestId": "req_abc123"
}See The chat fragment for the chat object’s field reference. createdAt is the 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. chat.providerChatGuidis the WhatsApp group JID (<id>@g.us) — use it on the path-parameterised/v1/chats/:chatId/*endpoints.chat.chatId(UUID) is whatPOST /v1/messagesaccepts aschatId.
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": {
"chat": {
"chatId": "550e8400-e29b-41d4-a716-446655440000",
"providerChatGuid": "iMessage;+;chat1234567890",
"channel": "imessage",
"isGroup": true,
"name": "Launch Team",
"participants": [
{ "handle": "+14155551234", "contactId": null, "role": "member", "isPhoneResolved": true },
{ "handle": "+14155559876", "contactId": null, "role": "member", "isPhoneResolved": true }
]
}
},
"requestId": "req_abc123"
}See The chat fragment for the field reference.
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": {
"chat": {
"chatId": "550e8400-e29b-41d4-a716-446655440000",
"providerChatGuid": "iMessage;+;chat1234567890",
"channel": "imessage",
"isGroup": true,
"name": "Weekend plans",
"participants": [
{ "handle": "+14155551234", "contactId": null, "role": "member", "isPhoneResolved": true }
]
},
"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": {
"chat": {
"chatId": "550e8400-e29b-41d4-a716-446655440000",
"providerChatGuid": "iMessage;+;chat1234567890",
"channel": "imessage",
"isGroup": true,
"name": "Launch Team",
"participants": [
{ "handle": "+14155550100", "contactId": null, "role": "member", "isPhoneResolved": true }
]
},
"participant": { "handle": "+14155550100", "contactId": null, "role": "member", "isPhoneResolved": true }
},
"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": {
"chat": {
"chatId": "550e8400-e29b-41d4-a716-446655440000",
"providerChatGuid": "iMessage;+;chat1234567890",
"channel": "imessage",
"isGroup": true,
"name": "Launch Team",
"participants": []
},
"removedParticipant": { "handle": "+14155550100", "contactId": null, "role": "member", "isPhoneResolved": true }
},
"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": "550e8400-e29b-41d4-a716-446655440000", "left": true },
"requestId": "req_abc123"
}chatId here is the conversation UUID.
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": "550e8400-e29b-41d4-a716-446655440000", "read": true },
"requestId": "req_abc123"
}chatId here is the conversation UUID.
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": "550e8400-e29b-41d4-a716-446655440000", "unread": true },
"requestId": "req_abc123"
}chatId here is the conversation UUID.
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": "550e8400-e29b-41d4-a716-446655440000", "typing": true },
"requestId": "req_abc123"
}chatId here is the conversation UUID. Works on both channels. On WhatsApp, "start" maps to the composing presence state and "stop" maps to paused.
Participants
Every participant entry on chat.participants[] follows the same shape, on both iMessage and WhatsApp:
| Field | Type | Description |
|---|---|---|
handle | string | Provider-native address. iMessage: E.164 phone or Apple ID. WhatsApp: <digits>@s.whatsapp.net (phone-resolved) or <id>@lid (LID-only, phone not yet known). The literal "self" denotes the customer’s own instance |
contactId | string | null | The customer’s contact UUID when the handle resolves to a known contact; null otherwise |
role | string | "member", "owner", or "self" |
isPhoneResolved | boolean | true when handle is a phone-based identifier or "self"; false when only a @lid is known. Treat this — not the format of handle — as the source of truth on whether the phone is known |
A WhatsApp participant may transition from isPhoneResolved: false to isPhoneResolved: true over the lifetime of the group as their phone number becomes known. Key your local store off whichever identifier you receive first; subsequent webhooks reuse the same logical participant.