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. Writing first comes with the $40 a month Outbound add-on on a dedicated line, 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 writing first comes with the $40 a month Outbound add-on on a dedicated line, and the conversations it starts ramp from 2 a day to 40 a day over 21 days. 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 | 1 to 10, reply and URL buttons | 1 to 3 reply buttons, or one URL button | No |
| Templates | No | Yes | No |
| Reactions | Telegram’s reaction emoji | Any one emoji | Any one emoji; not in groups |
| Typing indicator | Yes | Within 24 hours of the contact’s last message | For 5 minutes after your line’s last message (send a reply first) |
| 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. What you send is what the person gets: a kind or field the channel does not take fails with 422 unsupported_content and nothing is sent. Its param names what was refused and its hint says what to send instead, so choosing the substitute is your code’s job:
import { buttons, UnsupportedContentError } from "@flow-engineer/messaging";
try {
await event.conversation.send(buttons("When should we deliver?", ["Today", "Tomorrow"]), { idempotencyKey: `${event.id}:when` });
} catch (err) {
if (!(err instanceof UnsupportedContentError)) throw err;
// iMessage has no buttons: ask in words instead, with a new idempotency key
await event.conversation.send("When should we deliver: today or tomorrow?", { idempotencyKey: `${event.id}:when:basic` });
}
The 422 is kept under its idempotency key like any answer, so the substitute needs a new key: the refused one plus what it is.
Know before you send
You do not have to memorise the table. Each endpoint’s page in Flow’s API reference opens with the channels it works on and their limits, and the content types page has them all in one table. Branch on event.conversation.channel to choose content before sending, and handle unsupported_content and outside_window for what slips through.
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 555 0123 and 15555550123 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 trial on the shared line for 25 people, a $25 a month shared line for 300 people a month and 200 a day, and a $100 a month dedicated line. 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; writing first comes with the $40 a month Outbound add-on on a dedicated line. A Telegram bot can only message people who started it.
Do buttons work on iMessage?
No. iMessage has no buttons, and Flow converts nothing: a buttons message to an iMessage conversation is refused with 422 unsupported_content and nothing is sent. Send a basic message that lists the choices, and read the answer as text.
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?
Read the docs. Each kind of message has its own endpoint, and its page opens with a table of the channels it works on and their limits. A kind or field the conversation's channel does not take answers 422 unsupported_content, whose param names it and whose hint says what to send instead.
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