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). Responses return a canonical chat object — see The chat fragment for the field reference. The chatId path parameter on these endpoints is the provider-assigned chat identifier surfaced on chat.* webhook events.

Path chatId format differs per channel. iMessage chats use iMessage;+;<guid>; WhatsApp groups use <id>@g.us. The chat.chatId UUID returned in responses is the channel-independent identifier — use it as chatId on POST /v1/messages to send into a chat.

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.

The chat fragment

Every /v1/chats/* response embeds data.chat, a single canonical object that also appears on message.* and chat.* webhook events:

FieldTypeDescription
chatIdstring (UUID)Stable identifier for the conversation. The same value across channels. Pass this as chatId on POST /v1/messages to send into the chat
providerChatGuidstring | nullProvider chat identifier — iMessage;+;<guid> or <id>@g.us. The path-parameter chatId on /v1/chats/:chatId/* endpoints
channelstringimessage, sms, or whatsapp
isGroupbooleantrue for group chats, false for 1:1
namestring | nullGroup display name. null for 1:1 chats
participantsobject[]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

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": {
    "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:

  • 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.
  • chat.providerChatGuid is the WhatsApp group JID (<id>@g.us) — use it on the path-parameterised /v1/chats/:chatId/* endpoints. chat.chatId (UUID) is what POST /v1/messages accepts as chatId.

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

FieldTypeRequiredDescription
namestringYesNew 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

FieldTypeRequiredDescription
participantstringYesPhone 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_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": "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_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": "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

FieldTypeRequiredDescription
statusstringYes"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:

FieldTypeDescription
handlestringProvider-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
contactIdstring | nullThe customer’s contact UUID when the handle resolves to a known contact; null otherwise
rolestring"member", "owner", or "self"
isPhoneResolvedbooleantrue 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.