basic (text, or media with a caption), voice, buttons, list, carousel, template, contact_card, location, effect or reaction. Each kind you send has its own endpoint, named after it, whose body is the kind object itself. The same kind objects, with kind, describe what you receive (Message.content), webhook replies and stream frames.
TypeScript
curl
What each channel takes
Each endpoint’s page opens with its channels and limits; this is the summary. The WhatsApp column applies on WhatsApp numbers connected to your app (your own, or one the Flow team connects).text is at most 4096 UTF-16 code units on Telegram (an emoji such as 😀 counts as 2), 4096 characters on WhatsApp, 9999 on iMessage and 10 segments on SMS; a caption at most 1024. Longer text is refused with 400 invalid_request. Files: Telegram 50 MB, WhatsApp and iMessage 100 MB, MMS 5 MB.
Exact or 422
What you send is what the contact gets. Flow never converts a message into another kind, never drops a field and never answers2xx for something it did not send. A kind, field, channel block or value the conversation’s channel does not take fails with 422 unsupported_content, and nothing is sent:
param names what was refused (kind, a field such as reply_to or telegram.is_big, or a value’s field such as emoji or buttons), channel the conversation’s channel, and channel_code a few cases that depend on the conversation (group_unsupported, not_on_sms, no_mms, the edit and unsend windows). hint says what to send instead. Choosing the substitute is your code’s job, per channel:
A 422 is kept under its
Idempotency-Key, so send the substitute with a new key: the refused one plus what it is, such as evt_123:card:2:basic (Idempotency). A field this endpoint does not know at all is 400 invalid_request naming it.
Moving code from /v1 (content, fallback, delivered_as, GET /v1/capabilities)? The flow-messaging-migrate skill does it with your coding agent: claude plugin marketplace add flow-engineer/sdk, then claude plugin install flow-messaging@flow-engineer (source).
Styles, subjects and link previews
text is sent exactly as written on every channel: Flow reads no markdown, strips no marks and gives no warning. Style it in the channel’s own terms, in a block named after the channel:
- Telegram:
telegram.parse_modeHTMLorMarkdownV2, Telegram’s own formatting. Its length limits count the text after parsing. The block also takesdisable_notification,protect_content, and on mediashow_caption_above_mediaandhas_spoiler. - iMessage:
imessage.styles, ranges of the text (startandlengthin UTF-16 code units) withbold,italic,underline,strikethrough,big,smallor the animationsshake,nod,explode,ripple,bloomandjitter.imessage.subject(up to 200 characters) is the bold subject line. Text that looks like markup shows as written. - WhatsApp: the app reads its own
*bold*and_italic_by itself. No block. - SMS: plain text. No block.
422 with param the block’s name (telegram); an unknown key in a block, 400 invalid_request.
Link previews. link_preview on a basic message: left out, the channel’s default (Telegram, WhatsApp and iMessage show previews); false sends links as plain text. true on SMS, or with media, answers 422 with param link_preview.
iMessage lines with the SMS/RCS add-on. A message to a contact who cannot get iMessage goes by SMS or RCS; the message’s network says which. It shows no styles, subject or effect, and cannot be edited or unsent (422, channel_code not_on_sms).
What you receive
Inbound messages arebasic (text and/or one file), voice, button_reply, reaction, location, contact_card and file_blocked, read by content.kind. Each carries from, the contact who wrote it (in an iMessage group chat, which participant), and on iMessage network (imessage, sms or rcs).
- Media and voice notes always come with the file:
media.url(a voice note’surl; a Flow file URL that redirects to short-lived signed bytes) andfile_id, plusmime_typeandsize_bytes. Fetch it with your API key, or withflow.files.download(url); send it again by itsfile_id. - Voice note transcripts: Flow makes no transcripts of its own on any channel, and the app setting
transcriptionis not used yet. On iMessage a received voice note carries atranscriptwhen iMessage made one; no other channel has one. The audio is always included. - Taps on a reply button, or on a list row, arrive as
button_replywithbutton_id(the button’s or row’sid),label(its label or the row’s title) andmessage_id. file_blockedmeans the person sent a file Flow did not keep: its type is not passed on (programs, web pages), it was too large, it failed the malware scan, or Flow could not scan it (scan_failed: ask them to send it again).reasonsays which,media_typewhat the channel said it was, and there is nourl.- Documents are malware-scanned. Documents and archives people send you (PDF, Office files, zip and the like) are scanned before Flow keeps them; images, audio and video are not.
contentText(content) returns the readable text of any message: the text, a caption, a transcript (when there is one) or a tapped button’s label.
Sending files
Givemedia an HTTPS url Flow can fetch, or upload the file first and send its file_id:
curl
channel set, the file is checked against that channel’s limit at once. Documents and archives are malware-scanned before Flow stores them: one that fails answers 422 file_blocked and is not stored, and if the scan cannot run just then the upload answers a retryable 500 api_error with Retry-After (retry with the same Idempotency-Key; nothing was stored). media.type is image, video, document or audio; media.filename names a document (on Telegram only with file_id). Files are kept for your plan’s retention period (30 days by default).
Typing, read receipts, edits and reactions
In a webhook reply or a stream frame, these are kinds too:
typing, read, reaction, edit and unsend (the last two with the target in message_id).
Typing has no “off”: the body is empty ({}; on iMessage imessage.duration_seconds, 1 to 60), and no channel clears the indicator on request. It clears itself when your next message goes or after a few seconds, so call again while your agent works, or use conversation.responding(fn). Typing and read receipts go to the channel at once, not behind queued messages, so they can fail with 409 outside_window (for example iMessage typing more than 5 minutes after the contact’s last message) or 502 channel_error. Both are safe to ignore: never hold back a reply because of them. On iMessage, marking read marks the whole conversation, so up_to there answers 422 with param up_to. In an iMessage group, reactions, typing and read receipts answer 422 with channel_code group_unsupported.
Edit and unsend limits
Flow checks each edit and unsend when you call it, against its own record of when the message was sent (with a few seconds’ allowance at the limit). Past the channel’s limit the call is refused with422 unsupported_content and a channel_code:
The error’s
message states the limit and the message’s age, for example “iMessage allows unsends within 2 minutes of sending; this message was sent 6 minutes ago.” WhatsApp and SMS refuse every edit and unsend (param kind). Flow never sends a substitute: to correct a message you can no longer change, send a new one. An edit’s text is sent as written, styled with telegram.parse_mode or imessage.styles.
A 200 means Flow accepted the change, not that it is done. The change goes out in order, behind the conversation’s earlier messages:
- Done:
message.sentarrives for theeditorunsendmessage; itscontent.message_idnames the original. After an unsend, the original’sstatusisunsent. - Refused by the channel: if the channel still refuses after Flow accepted the change (for example it waited in the queue until the limit passed),
message.failedarrives for theeditorunsendmessage with the channel’s reason inerror. The original is unchanged.
Related
- Streaming replies: turning model output into chat bubbles.
- API: Send a basic message, Send text with buttons, Upload a file.