Skip to main content
Every message carries exactly one piece of typed content, selected by content.type; the same union is used for what you receive and what you send.
TypeScript
HTTP body

What each channel shows

WhatsApp is not available yet; its column shows what it will support. Text is at most the channel’s max_text_length: 4096 characters on Telegram and WhatsApp, 9999 on iMessage; longer text is refused with invalid_request. Telegram counts UTF-16 code units of the text as shown (after markdown), so an emoji such as 😀 counts as 2. A media caption takes at most 1024, also when an edit replaces it. Ask the API instead of this table: GET /v1/capabilities?conversation=conv_... returns, per content type, native, fallback (with what auto sends) or unsupported, plus the channel’s size limits and whether the window is open.

Nothing is converted silently

When a channel cannot show some content:
  1. Without fallback, the send fails with 422 unsupported_content. Nothing is sent.
  2. With fallback: "auto", Flow sends the documented default from the table above.
  3. With fallback set to a content object, Flow sends exactly that instead.
When what the person sees differs from what you sent, the message carries delivered_as:
A reply to numbered-text buttons (“2”) still arrives as a button_reply with the button’s id, so your code handles taps the same way on every channel.

Content you receive

Inbound messages use text, media, voice, button_reply, reaction, location, contact_card and file_blocked.
  • Media and voice notes always come with the file: url (a Flow file URL that redirects to short-lived signed bytes) and file_id. Fetch it with your API key, or with flow.files.download(url).
  • Voice note transcripts: Flow does not transcribe voice notes yet (the app setting transcription cannot be turned on). On iMessage, a voice note may carry a transcript when the channel provides one; on Telegram it never does. The audio is always included.
  • Button taps arrive as button_reply with button_id and label.
  • file_blocked means the person sent a file Flow did not keep: its type is not passed on (programs, web pages), it was too large, or (once malware scanning is on) it failed or could not get the scan. The reason says which, and there is no url.
  • Documents are not malware-scanned yet. Documents and archives people send you (PDF, Office files, zip and the like) are kept unscanned: treat them as untrusted.
In TypeScript, contentText(content) returns the readable text of any content: the text, a caption, a transcript (when there is one) or a tapped button’s label.

Sending files

Give media as an HTTPS url Flow can fetch, or upload it first and send its file_id:
curl
Files up to 100 MB are taken; with channel set, the file is checked against that channel’s limit at once. Until malware scanning is on, uploads of documents and archives (PDF, Office files, zip and the like) are refused with 422 unsupported_content, and so are sends of documents people sent you; images, audio and video are not affected. Files are kept for your plan’s retention period (30 days by default).

Typing, read receipts, edits and reactions

These are content too, so they work the same way over HTTP, the live stream and webhook replies. Over HTTP each also has its own endpoint: Typing indicators clear themselves after a few seconds on every channel; keep turning them on while your agent works, or use conversation.responding(fn), which does it for you and always turns typing off. 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 (the channel failed or timed out). Both are safe to ignore: never hold back a reply because of them. On iMessage, marking read marks the whole conversation, and up_to has no effect.

The escape hatch

channel_options adds channel parameters for how a message looks and notifies, for example {"parse_mode": "HTML"} on Telegram. Each channel has an allowlist: keys not on it are dropped, never passed to the channel, and the typed request wins where both set the same thing. Who receives the message and what it says always come from the typed request; keys starting with _flow_ are refused with invalid_request. Flow does not validate the values, and they may break when a channel changes. Prefer typed content.