Skip to content

Ask the docs

Answers come only from these docs and are written by an AI model (Gemini), so check the linked pages. Your question is not kept. Do not include personal details or keys.

iMessage

iMessage · iMessage API for AI agents · iMessage AI agent · send iMessage from API · iMessage chatbot API · AI agent SMS iMessage · iMessage business API for developers · blue bubble API

Receive and answer iMessage, one-to-one and in group chats, on Flow's shared line or a dedicated line.

Let people text your agent on an iMessage line. The person writes first; your agent replies with the same code you use on Telegram. iMessage has no 24-hour window: once someone has texted your line, your agent can message them any time.

TypeScript
// npm install @flow-engineer/messaging
import { FlowMessaging, contentText } from "@flow-engineer/messaging";
import { askMyAgent } from "./agent"; // returns a string, or any LLM stream
const flow = new FlowMessaging();
for await (const event of flow.events.stream({ types: ["message.received"] })) {
const text = contentText(event.data.message.content);
if (event.conversation.type !== "group") {
await event.conversation.react(event.data.message.id, "👍"); // shows as the Like tapback; groups take no reactions
}
await event.conversation.reply(await askMyAgent(text));
}
  1. Sign in to the dashboard with your Flow account. Your first sign-in (or the first key you make there) starts the account’s free trial, which opens Flow’s shared iMessage line to every app of the account, with test and live keys. The Flow CLI’s npx @flow-engineer/flow setup starts the same trial.
  2. GET /v0/senders?channel=imessage lists the line (kind shared) with your app’s join_code, the message a person sends to join your app. address.link, when set, opens Messages with the line and that message filled in.
  3. Run the code above, send the join message to the line from an iPhone or Mac, and your agent answers. A person’s messages go to your app until they send another app’s join message.

The trial is a free trial for 25 people on the shared iMessage line (see Pricing); an account that moves to a paid plan keeps the line, with that plan’s limits. The shared line ignores group chats and never writes first.

What a line can do beyond replying is on the sender, in imessage (read-only; it follows the line and its add-on): fallback (SMS/RCS fallback, included on every line, section 6), call_forwarding (section 8) and can_initiate (writing first, section 5).

Every message type you can send, with its API call and a screenshot from the contact’s iPhone, is on iMessage message types.

People read iMessage as short lines. reply() uses shorter bubbles here (about 400 characters before it cuts at a sentence end) than on Telegram. See Streaming replies.

  • Reactions (POST .../reaction): ❤️ 👍 👎 😂 ‼️ ❓ show as tapbacks, and any other emoji as an emoji reaction. Groups take none (422 unsupported_content, channel_code group_unsupported).
  • Effects (POST .../effect) send text with an iMessage effect:
TypeScript
import { effect } from "@flow-engineer/messaging";
await event.conversation.send(effect("Your order is confirmed!", "confetti"));

Effects are slam, loud, gentle, invisible_ink, echo, spotlight, balloons, confetti, love, lasers, fireworks and celebration. Which ones a contact’s device shows may vary, and a message that goes by SMS or RCS shows the text without the effect. Effects work on iMessage only: send the same text as a basic message on other channels.

4. No buttons: list the choices in the text

Section titled “4. No buttons: list the choices in the text”

iMessage has no buttons, lists or carousel cards: they answer 422 unsupported_content with param kind, and nothing is sent. Flow converts nothing, so send a basic message whose text lists the choices, and read the person’s answer, which arrives in words as a basic message. To show several photos or videos together, send one basic message with imessage.media_group (photo carousel).

TypeScript
await event.conversation.send("Which time works, 10:00 or 14:00?", { idempotencyKey: `${event.id}:time` });

If your code sends buttons on every channel, catch the 422 and send the text instead with a new Idempotency-Key: a 422 is kept under its key, so add what the substitute is, such as evt_123:time:basic. See unsupported_content.

iMessage lines are personal-style numbers, and iMessage watches closely for unwanted messages. Flow protects your line:

  • The person writes first. A line replies only, unless writing first is switched on for it (imessage.can_initiate). From a reply-only line, starting a conversation with a new contact (POST /v0/messages) is refused with 403 permission. Someone who has written to the line before is an open conversation: replies to them spend no new-contact budget, but each person still counts toward your plan’s people for the day and month (Pricing).
  • No 24-hour window. iMessage has no 24-hour window: once someone has texted your line, your agent can message them any time. Only typing and read receipts are timed: they work for 5 minutes after your line’s last message to the person, so send your reply first.
  • Writing first comes with the Outbound add-on, for dedicated lines only (Pricing). Buy it ($40 a month) in the dashboard with Dedicated, or later on the same card. The line then writes first within a warm-up: it starts 2 new conversations a day, growing to 40 a day over its first 21 days (limits), separately from its daily limit of different people. If the channel still refuses a start, the send ends in message.failed with outside_window. The shared line never writes first.
  • Get people to write with the opt-in link. When the line has one, the sender’s address.link opens Messages with the line and a prefilled text the person sends to start. Put it on your site, receipts or a QR code.
  • No cold outreach. Sending the same text to many people, or messages that get no reply, can throttle the line (sender_throttled).

See The send gate.

SMS/RCS fallback is included on every iMessage line: Flow’s shared line (on the free trial and the Shared plan) and dedicated lines alike, with nothing to buy. With it (imessage.fallback on the sender), a message to someone who cannot get iMessage goes by SMS or RCS from the same number. On a line without it, a message to someone not on iMessage fails (message.failed).

Your app never picks the network: it is chosen per message when it is sent. Message.network says which way a message went: imessage, sms or rcs. It is set on inbound messages, and on yours once the channel reports it (data.message.network in message.delivered or message.failed).

The network is chosen after Flow sends, so Flow sends every message as for iMessage: a message that goes by SMS or RCS shows no imessage.styles, no imessage.subject and no effect, and leaves out an imessage.line_contact_card card, so it arrives as its text alone. Message.network tells which way it went, and is the only sign the card was left out. Keep text readable without them. Edits and unsends of a message that went by SMS or RCS are refused with 422 unsupported_content, channel_code not_on_sms: send a new message instead.

Only dedicated lines take group chats, and only ones people add the line to. Flow’s shared line ignores group chats: they never reach your app from it.

  • A group is one conversation with type group. It has no contact; group holds the group’s name, when it has one, and its participants (each a contact on the channel, made the first time Flow sees them). Every inbound message carries from, the contact who wrote it.
  • People who join or leave change the list without an event. Read the conversation (GET /v0/conversations/{conversation_id}) for the current list; events inline only group_name.
  • Writers count toward your plan’s people; members who never write do not. A writer past a limit is held: their messages wait and arrive when they are let in, and the rest of the group’s messages are not held up. See Pricing.
  • Reply with the normal sends (POST /v0/conversations/{conversation_id}/basic and the other kinds’ endpoints): basic messages (with imessage.styles, imessage.subject and link_preview), media, voice notes, effects, threaded replies (reply_to), edits and unsends work in groups.
  • No reactions, typing or read receipts from your line. Each answers 422 unsupported_content with param kind and channel_code group_unsupported, and nothing is sent: leave them out in groups.
  • Flow cannot create a group, rename one, or add or remove people. A start (POST /v0/messages) always makes a direct conversation.

Test groups before you have a line: POST /v0/test/conversations with "channel": "imessage" and a group (name, participants) makes a simulated group conversation under the same rules. See Test and live mode.

  • Phone calls to a dedicated line’s number can be forwarded to a phone you choose; call forwarding is included with Dedicated and set up with the Flow team (imessage.call_forwarding.phone shows where). Forwarded calls send no event. The shared line does not forward calls.
  • FaceTime calls are never forwarded: the line cannot answer them, and each attempt arrives as a call.received event (data.call.contact, data.call.network facetime) in the direct conversation with the caller, made if it is new. A webhook endpoint receives it only when it lists call.received in events.
  • A call is not a message: it does not count toward your plan’s people, and it does not let a reply-only line write to the caller first.
Section titled “9. Text styles, subjects, link previews and your contact card”

text is sent exactly as written: Flow reads no markdown, and text that looks like markup shows as written. Style a basic message with imessage.styles, ranges of the text (start and length in UTF-16 code units) with a style: bold, italic, underline, strikethrough, big, small, or the animations shake, nod, explode, ripple, bloom and jitter. imessage.subject (up to 200 characters) shows as the message’s bold subject line, and link_preview: false sends links without a rich preview.

TypeScript
await flow.messages.basic(event.conversation.id, {
text: "Your order is on its way. Track it: https://example.com/t/4821",
imessage: { subject: "Order 4821", styles: [{ start: 14, length: 10, style: "big" }] },
link_preview: false,
});

imessage.line_contact_card: true attaches your line’s own contact card: a card bubble with the line’s name, photo and number, which the contact taps to save. Send it when a contact asks for your card, or once early in a conversation, not on every message.

{ "text": "Save my contact card so you can find me", "imessage": { "line_contact_card": true } }
  • Dedicated lines only. On a shared line it answers 422 unsupported_content with param imessage.line_contact_card and channel_code line_contact_card_dedicated_only.
  • It needs text and cannot go with media. Either answers 400 invalid_request with param imessage.line_contact_card.
  • By SMS or RCS the card is left out. The message arrives as its text alone, and only its network (sms or rcs) tells you.

The imessage block works on iMessage only: on another channel it answers 422 unsupported_content with param imessage. See Send a basic message.

KindiMessage
Text (basic)yes, up to 9999 characters, sent as written; styles, a subject and, on a dedicated line, your line’s contact card through the imessage block (section 9)
Images, video, audioyes
Documentsyes, sent and received; malware-scanned before Flow keeps them (an upload that fails answers 422 file_blocked)
Reactionsyes: tapbacks for ❤️ 👍 👎 😂 ‼️ ❓, emoji reactions for others; not in groups
Typingyes, once the contact wrote, for 5 minutes after your line’s last message to them, so send your reply first (otherwise 409 outside_window, safe to ignore); not in groups (422, group_unsupported)
Read receiptsyes, for the whole conversation (up_to answers 422 unsupported_content), once the contact wrote, for 5 minutes after your line’s last message to them, so send your reply first (otherwise 409 outside_window); not in groups (422, group_unsupported)
Effectsyes (may be lost on a message that goes by SMS or RCS)
Photo carouselyes: 2 to 10 images and videos in one message with imessage.media_group on a basic message
Buttons, lists, carousel cards (/carousel), templatesno (422 unsupported_content): list the choices in a basic message’s text; for photos, use imessage.media_group
Edityes, within 15 minutes of sending (otherwise 422 unsupported_content, edit_window_closed); not the message that started the conversation (first_message_not_editable), nor one that went by SMS or RCS
Unsendyes, within 2 minutes of sending (otherwise 422 unsupported_content, unsend_window_closed); not one that went by SMS or RCS
Voice notesyes, from an mp3, wav, m4a, caf or aac file
Contact cardsyes, as a basic message with a vCard (.vcf) as media of type document; your own dedicated line’s card with imessage.line_contact_card (section 9). The /contact-card endpoint answers 422 unsupported_content
Locationsno (422 unsupported_content): send a basic message with a maps link

Each endpoint’s page opens with the channels it works on and their limits. Each type’s request and how it looks on an iPhone: iMessage message types.

An iMessage contact’s address.handle is a phone number or an email address. Do not assume a phone number. Reply into the conversation the person started rather than addressing them by handle.