BetaThe V4 API is in beta — endpoints and functionality may change.

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 it

Node.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 use app_clip; to deep-link into an already-installed app use rich_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 url is opaque to Blooio — it's whatever the sending app encoded. Your integration (or the app that owns that bundle_id) is responsible for decoding it and deciding what to do. Received bubbles can come from any iMessage app a contact used, so branch on bundle_id / team_id to 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:

  1. Receive a message.received event with message_type: "imessage_app".
  2. Branch on bundle_id / team_id to confirm it's an app you support.
  3. Decode url with your own logic to read the current state.
  4. Reply by sending a new imessage_app bubble whose url carries the next state (reuse the same bundle_id / team_id).

Use session_id to correlate the messages that belong to the same exchange.