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. |
app_store_id |
no | App Store id (adam id) for the "get this app" link when the recipient doesn't have your app. |
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.
How the card looks — and what happens on tap
This is the part that surprises people, so read it before you pick imessage_app.
The fallback card is compact. When the recipient does not have your app,
Messages renders a small "thumbnail + caption" row — a little leading image (from
image_url) next to caption / subcaption. It is not a large hero-image
card, and there is no way to make the fallback card big; that layout is fixed
by Messages. A large edge-to-edge image only appears inside your app's own
interactive view, which only recipients who have your app installed will see.
Tapping is app-gated. Tapping the bubble opens your app only if the
recipient has your exact extension installed. If they don't, Messages sends them
to the App Store (to the app_store_id listing if you provided one). It does
not open url in a browser — url is app state, only meaningful to your
extension. So a custom bubble can't "open a link" for someone who doesn't have
your app.
If you want a big, tappable card instead
If your goal is a marketing/notification card — a large image that opens content when tapped, on every device, with no app required — use a different content type:
| You want… | Use | Tap behavior |
|---|---|---|
| A large image card that opens a link | rich_link |
Opens the URL |
| An instant, no-install mini experience | app_clip |
Launches an App Clip |
| Your own interactive app bubble | imessage_app |
Opens your app if installed, else the App Store |
What to include (and skip)
- Always set
caption(and optionallysubcaption) — it's the text every recipient sees on the fallback card. - Set
app_nameso the fallback card is attributed to your app. - Set
image_urlfor the small leading thumbnail. Keep it square and small (thumbnail-sized); it is not rendered as a full-width banner, so a large hero image is wasted here — userich_linkif you need that. - Set
app_store_idso recipients without your app land on the correct App Store listing rather than a generic "not available" state. - Don't rely on
urlto open anything for recipients who lack your app — it is only handed to your extension. Put anything a general audience should be able to open in arich_linkinstead.
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 the
badgeattribution - Link previews — rich link cards (
rich_link) - Webhooks — subscribe to
message.received - Webhook signatures — verify inbound webhooks