Custom bubbles (iMessage apps)
Send and receive bring-your-own iMessage app extension bubbles
A custom bubble is a Messages app-extension bubble — the rich, tappable card an iMessage app renders inline in a conversation. Blooio lets you send a bubble that points at your own iMessage app extension, and receive the bubbles your contacts send back as structured data.
Note
Custom bubbles are Blooio iMessage only (peer-to-peer). Blooio is the transport, not the renderer: recipients who have the app installed see its interactive card; everyone else sees a native template card with Apple's built-in App Store fallback.
Sending a custom bubble
Send the imessage_app content type. You identify your own signed iMessage
app extension with bundle_id + team_id, and pass the app-state url your
extension reads when the recipient taps the bubble.
| Field | Required | Description |
|---|---|---|
bundle_id |
yes | Your iMessage extension's bundle id (e.g. com.example.app.imessage). |
team_id |
yes | Your Apple Developer Team ID. Combined with bundle_id to form the balloon plugin id. |
url |
yes | The app-state string your extension understands. Opaque to Blooio — you define its meaning. |
app_name |
no | Name shown on the template card (the not-installed fallback). |
caption |
no | Caption line on the template card. |
subcaption |
no | Secondary line on the template card. |
image_url |
no | Image shown on the template card. |
cURL
curl -X POST 'https://api.blooio.com/v4/channels/CHANNEL_ID/messages' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"to": "+15551234567",
"imessage_app": {
"bundle_id": "com.example.app.imessage",
"team_id": "TEAMID1234",
"url": "https://example.com/state?s=eyJzdGVwIjoxfQ",
"app_name": "Example App",
"caption": "Your turn!"
}
}'Try itNode.js
await fetch('https://api.blooio.com/v4/channels/CHANNEL_ID/messages', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: '+15551234567',
imessage_app: {
bundle_id: 'com.example.app.imessage',
team_id: 'TEAMID1234',
url: 'https://example.com/state?s=eyJzdGVwIjoxfQ',
app_name: 'Example App',
caption: 'Your turn!',
},
}),
})Python
requests.post(
'https://api.blooio.com/v4/channels/CHANNEL_ID/messages',
headers={
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
json={
'to': '+15551234567',
'imessage_app': {
'bundle_id': 'com.example.app.imessage',
'team_id': 'TEAMID1234',
'url': 'https://example.com/state?s=eyJzdGVwIjoxfQ',
'app_name': 'Example App',
'caption': 'Your turn!',
},
},
)Note
The interactive overlay only renders for recipients who have your app installed — Blooio does not host or ship a renderer. Everyone else sees the template card built from
caption/app_name/image_url, plus Apple's App Store fallback. For an instant, no-install experience useapp_clip; to deep-link into an already-installed app userich_link.
Receiving a custom bubble
When a contact sends a custom bubble back, its text is just a placeholder glyph
on the wire — the meaningful content lives in the bubble's payload. Blooio
decodes it for you and delivers a message.received webhook with
message_type: "imessage_app" and a structured imessage_app object (symmetric
to the send shape), so you never have to parse Apple's archive yourself.
| Field | Description |
|---|---|
balloon_bundle_id |
Full Messages plugin id: com.apple.messages.MSMessageExtensionBalloonPlugin:<team_id>:<bundle_id>. |
team_id |
The sending app's Apple Developer Team ID. |
bundle_id |
The sending app's iMessage extension bundle id — use it to route to the right handler. |
app_store_id |
App Store numeric id, when present. |
app_name |
App name carried on the bubble. |
url |
The app-state string the sender's extension wrote. This is the payload — decode it in your own app logic. |
caption |
Caption line, if any. |
subcaption |
Secondary line, if any. |
summary |
Short summary / accessibility label. |
layout_class |
Messages layout class (e.g. MSMessageTemplateLayout). |
session_id |
Session identifier that correlates a multi-step exchange (e.g. the turns of a back-and-forth), when present. |
The top-level text mirrors the bubble's caption so text-only consumers still
see something readable instead of a placeholder.
{
"type": "message.received",
"created_at": 1730000000000,
"organization_id": "org_EXAMPLE",
"data": {
"kind": "received",
"message_type": "imessage_app",
"text": "Your turn!",
"sender": "+15551234567",
"recipient": "+15557654321",
"channel_address": "+15557654321",
"chat_id": "chat_EXAMPLE",
"message_id": "msg_EXAMPLE",
"protocol": "imessage",
"attachments": [],
"imessage_app": {
"balloon_bundle_id": "com.apple.messages.MSMessageExtensionBalloonPlugin:TEAMID1234:com.example.app.imessage",
"team_id": "TEAMID1234",
"bundle_id": "com.example.app.imessage",
"app_store_id": 1234567890,
"app_name": "Example App",
"url": "https://example.com/state?s=eyJ0dXJuIjoyfQ",
"caption": "Your turn!",
"subcaption": null,
"summary": "Example App",
"layout_class": "MSMessageTemplateLayout",
"session_id": "3F2504E0-4F89-41D3-9A0C-0305E82C3301"
}
}
}Note
The
urlis opaque to Blooio — it's whatever the sending app encoded. Your integration (or the app that owns thatbundle_id) is responsible for decoding it and deciding what to do. Received bubbles can come from any iMessage app a contact used, so branch onbundle_id/team_idto handle the ones you support and ignore the rest.
Building a stateful experience
url + session_id are enough to build a full back-and-forth on top of a custom
bubble:
- Receive a
message.receivedevent withmessage_type: "imessage_app". - Branch on
bundle_id/team_idto confirm it's an app you support. - Decode
urlwith your own logic to read the current state. - Reply by sending a new
imessage_appbubble whoseurlcarries the next state (reuse the samebundle_id/team_id).
Use session_id to correlate the messages that belong to the same exchange.
Related
- Find My & Check In bubbles — Apple's first-party iMessage apps
- Attachments & carousels — media bubbles and
sent_from_siri - Link previews — rich link cards (
rich_link) - Webhooks — subscribe to
message.received - Webhook signatures — verify inbound webhooks