Build an agentiMessage API8 min read
One agent on iMessage, WhatsApp and Telegram
Run one AI agent across iMessage, WhatsApp and Telegram with Flow. Keep one codebase while Flow handles each channel's windows, buttons, limits and identity.

The short answer
Flow lets one agent answer on iMessage, WhatsApp and Telegram from one codebase. Your tools and logic stay the same while Flow enforces each channel's rules around first messages, buttons, text length and identity.
Building on the iMessage API? Build on iMessage with Flow: one agent for iMessage, WhatsApp and Telegram. New to it? Read the iMessage API guide.
One handler, three channels
The promise of a unified messaging layer is that the code that answers a customer does not care where the customer is. This is the whole loop on Flow, from the quickstart:
import { FlowMessaging, contentText } from "@flow-engineer/messaging";
const flow = new FlowMessaging();
for await (const event of flow.events.stream({ types: ["message.received"] })) {
const said = contentText(event.data.message.content);
await event.conversation.reply("You said: " + said, { idempotencyKey: event.id });
}
The same loop receives a message from a Telegram bot, a WhatsApp number or an iMessage line, and replies into the same conversation. event.conversation.channel tells you which one if you want to change the tone.
The differences are not in the code. They are in the rules each channel enforces, and a messaging layer should apply them for you so a refused send comes back as a clear error.
Who can write first
| Channel | You may send free-form content when… | Otherwise |
|---|---|---|
| The contact’s last message is less than 24 hours old | Only an approved template | |
| iMessage (Flow) | The contact has written to this line before | Customers text first. A dedicated line with the outbound add-on can write first, within warm-up limits |
| Telegram | The contact has started your bot | Telegram refuses the message |
This is from Flow’s sending rules and its iMessage plans. The practical lesson is the same on all three: agents that reply to people who wrote first rarely meet these rules. Agents that broadcast do.
Flow keeps the channel rules at the messaging boundary: customers text the shared iMessage line first, while a dedicated line can support writing first subject to availability and warm-up limits. Your agent logic stays the same across channels.
What each channel can show
From Flow’s content types docs:
| Content | Telegram | iMessage | |
|---|---|---|---|
| Text | Yes, up to 4,096 characters | Yes, up to 4,096 | Yes, up to 9,999 |
| Images, video, audio, documents | Yes | Yes | Yes |
| Buttons | Inline keyboard | Up to 3 buttons, else a list | No |
| Templates | No | Yes | No |
| Reactions | Yes | Yes | Tapbacks, a fixed set |
| Typing indicator | Yes | Within 24 hours of the contact’s last message | Within 5 minutes |
| Edit a sent message | Yes | No | Within 15 minutes |
| Unsend | Within 48 hours | No | Within 2 minutes |
| Message effects | No | No | Yes |
Nothing is converted silently. Without a fallback, a send that the channel cannot show fails with unsupported_content. With fallback: "auto", Flow sends the documented default, such as numbered text for buttons on iMessage. The message then carries a delivered_as field that says what the person actually saw.
import { buttons } from "@flow-engineer/messaging";
await conversation.send({
content: buttons("When should we deliver?", [
{ id: "today", label: "Today" },
{ id: "tomorrow", label: "Tomorrow" },
]),
fallback: "auto", // iMessage has no buttons: send numbered text instead
});
When the person answers “2” on iMessage, you still receive a button_reply with the second button’s id, so your code handles taps the same way everywhere.
Ask before you send
You do not have to memorise the table. GET /v1/capabilities?conversation=conv_... returns, per content type, native, fallback or unsupported, plus the channel’s size limits and whether the window is open. Use it to choose content before sending, instead of handling unsupported_content and outside_window afterwards.
For retries, send an Idempotency-Key with every POST. Retry only what clears by itself: rate_limited and sender_throttled carry a retry_after, and outside_window will not clear until the person writes again.
One person, many channels
Customers do not think in channels. The same person may text on iMessage and message on WhatsApp. Joining those identities is a security decision, so do it deliberately.
Flow’s framework docs say phone numbers on SMS and WhatsApp are normalised to E.164, so +1 555 010 0100 and 15550100100 identify the same phone. For other interfaces you supply a resolve function that maps the verified channel identity to your own account key. Flow does not guess that an app user and a phone number are the same account.
An iMessage handle can be a phone number or an email address, so do not assume a number. Never link identities by a matching display name.
Where this fits with Flow
Flow is the framework and messaging API this post is based on. You write your product’s tools once in TypeScript and the same agent answers on iMessage, WhatsApp and Telegram, and works as an MCP server and CLI for ChatGPT, Claude and coding agents. iMessage starts with a free shared line for up to 25 people a month, a $30 a month shared line for roughly 200 to 300 people and dedicated lines from $100 a month. See iMessage API pricing.
For Flow’s iMessage plans and limits, see Flow on iMessage: features, pricing and limits. For the full agent design, read Build an iMessage AI agent, or start with a test key on the Telegram sandbox.
Frequently asked questions
Can one AI agent work on iMessage, WhatsApp and Telegram?
Yes. With a messaging layer that normalises events and applies each channel's rules, one handler receives messages and replies on all three. Flow's docs say the same code answers on Telegram, WhatsApp and iMessage.
What is the biggest difference between the channels?
Who can write first. WhatsApp allows free-form replies for 24 hours after the contact writes, and then only approved templates. On Flow's iMessage line, customers text first unless a dedicated line has the outbound add-on. A Telegram bot can only message people who started it.
Do buttons work on iMessage?
No. iMessage has no buttons. With fallback set to auto, Flow sends numbered text, and when the person answers with the number you still receive a button reply with the button's ID.
What are the text length limits?
4,096 characters on Telegram and WhatsApp, and 9,999 on iMessage. Longer text is refused, so check the limit or let a reply helper split the answer.
How do I know what a channel supports?
Ask the API. GET /v1/capabilities for a conversation returns, for each content type, whether the channel shows it natively, through a fallback or not at all, plus size limits and whether the window is open.
How do I match the same person across channels?
Only through a verified mapping. Phone numbers on SMS and WhatsApp are normalised to E.164. For other interfaces such as iMessage, supply a resolve function that maps the verified identity to your own account key. Flow does not guess that two identities are the same person.
Keep reading
All posts: the Flow blog. The Flow docs have the reference.
From reading to building
Your agent, on iMessage and everywhere else.
Flow gives your agent an iMessage line, and the same code answers on WhatsApp and Telegram and works as an MCP server and CLI. See how it works on iMessage.
Build on iMessage with Flow