The key idea
Unify the business workflow, message lifecycle and tool interface. Keep channel-specific identities, permissions and delivery constraints visible at the adapter boundary.
Your app might need to answer the same question on WhatsApp, iMessage and Telegram: “Where is my order?” Building three separate implementations of that workflow creates three places to maintain the lookup, authentication and error handling.
A unified messaging layer gives your application a common way to handle conversations. Channel adapters translate the transport; shared tools perform the business operation. Your customer can use a familiar chat interface while your team keeps one source of truth for what the app does.
What is a unified messaging API?
A unified messaging API provides a shared contract for messages across multiple channels. Depending on the product, that contract can include sending, receiving, conversation identities, delivery status and agent execution.
The useful abstraction is larger than a send function. A customer asks a question, the application identifies them, a tool reads the order and the result becomes a reply. That entire workflow needs a coherent record even when the transport differs.
For an AI agent, the shared layer also decides which tools are available and how their results reach the conversation. The transport should not determine whether a customer is allowed to cancel an order; your application should.
Share the workflow, keep the channel rules
A common API should preserve important differences instead of hiding them. Recipient identity, customer initiation and available message formats belong to the channel boundary.
| Layer | Share across channels | Keep channel-specific |
|---|---|---|
| Customer task | Order lookup, booking update, support request | Conversation entry point |
| Identity | Verified application account and permissions | Telegram chat ID, phone number or provider conversation ID |
| Agent tools | API or MCP operations and confirmation rules | Incoming event and attachment translation |
| Messages | Internal message ID and lifecycle | Provider status and formatting capabilities |
| Delivery | Queue, retries and duplicate protection | Messaging window, provider limits and transport errors |
WhatsApp’s Business Platform has a customer-service window and template rules, described in WhatsApp’s policy. Telegram’s Bots FAQ explains customer initiation for private bot conversations. For iMessage, clarify the specific integration route; Apple’s Messages framework is distinct from a third-party server API.
The WhatsApp agent, Telegram agent and iMessage API guides explain those boundaries in more detail.
Connect your app once
Expose business operations through an API or MCP server. Name them by their outcome: find an order, check availability, create a booking or update a delivery address. Return structured results so the agent can distinguish success, a missing record and a business constraint.
For an appointment workflow, a useful tool contract might be:
find_bookingreturns the customer’s eligible booking.list_available_slotsreads the current schedule.move_bookingaccepts the selected slot and returns the saved booking.
These names illustrate operations your backend could expose; they are not a prescribed Flow SDK. Your existing API may use different names or routes. What matters is that the operation has a narrow purpose and an explicit result.
Keep customer confirmation separate from a successful write. “I can move it to Friday” is a proposal. “It is now booked for Friday” requires a saved update. Reusing that distinction across channels gives customers the same dependable behavior wherever they message.
With Flow Framework, you write these operations once as typed tools, and the same project serves an MCP server, a CLI and a messaging agent. npx @flow-engineer/flow new my-agent creates a starter project. Already have an agent? Flow’s Messaging API, below, moves its messages on every channel.
A working API example: a test key and a reply
Flow’s Messaging API at https://api.flow.engineer puts your agent on Telegram, WhatsApp and iMessage. It needs no account to start: one call returns a test key for the Telegram sandbox.
curl -X POST https://api.flow.engineer/v0/sandbox/keys
The answer holds a test key (fk_test_…, shown once; keep it on your server as FLOW_MESSAGING_KEY) and the sandbox bot’s link, which you open on your phone to start a conversation. Each incoming message carries its conversation ID (conv_…), and your agent replies into that conversation:
curl -X POST "https://api.flow.engineer/v0/conversations/$CONVERSATION_ID/basic" \
-H "Authorization: Bearer $FLOW_MESSAGING_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: order-1042-ready' \
-d '{"text": "Your order 1042 is ready for pickup."}'
Each kind of message has its own endpoint (/basic for text or one file, /buttons, /template, …), and a kind the channel does not take is refused with 422 unsupported_content rather than converted. An accepted send returns 202: queued, with its outcome arriving later as message.sent, message.delivered or message.failed. For coding agents, api.flow.engineer/llms.txt indexes the docs; the npm package is @flow-engineer/messaging, and npx @flow-engineer/messaging init gets a test key and writes it to .env.
Send customer updates through the same API
An update to an existing customer, such as “your order is ready”, goes into their conversation with the same call on every channel. The channel’s rules still apply, and the API says when one blocks the send:
- WhatsApp takes free-form messages only within 24 hours of the customer’s last message. Outside that window, send an approved template to
POST /v0/conversations/{conversation_id}/template; any other kind answers409 outside_window. - Telegram delivers only to people who have started your bot.
- iMessage lines reply to people who wrote to the line first. A dedicated line with the $40 a month Outbound add-on may also write first, to people who expect to hear from you.
curl -X POST "https://api.flow.engineer/v0/conversations/$CONVERSATION_ID/template" \
-H "Authorization: Bearer $FLOW_MESSAGING_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: order-1042-ready-template' \
-d '{"template_id": "tpl_01JB8Z9X2D4F6H8K0M2P4R6T8V", "language": "en", "params": {"body": ["Asha", "order 1042"]}}'
To message someone who has no conversation with your sender yet, POST /v0/messages takes a sender, a to and a basic or template message, within the same channel rules. Follow a queued message through its events, or read GET /v0/messages/{message_id} to tell queued, sent, delivered and failed apart. Replace the sample IDs with your own: test keys reach only people who joined the sandbox, and live keys send from your own senders.
Make retries and ordering part of the design
Networks fail after requests are accepted. Providers retry webhook events. A customer can send a correction while an earlier request is still running. Your messaging architecture should expect those situations.
Use stable event IDs for inbound deduplication and idempotency keys for outbound operations. Process messages in order within a conversation, while allowing different conversations to proceed independently. Keep a business transaction ID alongside the message ID so you can trace an update from the customer’s request to its stored result.
Flow’s Messaging API takes an Idempotency-Key header on every POST and keeps it for 24 hours. Reuse it for the same request body when retrying; changing the body under the same key answers 409 idempotency_conflict. Give each intended customer update its own key.
Treat permanent and temporary failures differently. A transient transport problem may justify a retry. An unknown customer, a disconnected channel or an expired WhatsApp window needs an application decision. Repeatedly sending the same request cannot repair a missing permission or an ineligible recipient.
Do not silently merge customer identities across channels. Link them through a verified account flow. Likewise, do not silently send a message through another channel because the first one failed: fallback changes the customer experience and should be an explicit product choice.
Choose your first channel and workflow
Start where your customers already contact you. Choose a task with a clear backend result, such as checking an order or moving a booking. Make that conversation work from request to completed operation before adding a second interface.
Then reuse the tool contract and adapt the channel boundary. Review recipient identity, formatting, attachments and delivery errors for the new channel. Compare completed tasks and support transfers by channel so you can see where the interface needs a different explanation.
A unified API is valuable when it reduces repeated work while preserving the details your product depends on. It should make adding another interface a smaller change, rather than making every channel behave as though it had the same rules.
Frequently asked questions
Does one API mean every channel has identical features?
No. A shared contract can normalize common operations while exposing channel-specific capabilities and constraints. Keep those differences explicit when they affect the customer’s task.
Can I reuse the same agent tools?
Yes. Keep the tools focused on application operations and make the messaging adapter responsible for translating channel events and replies.
Does 202 Accepted mean the customer received the message?
For Flow’s Messaging API, it means the message passed the channel’s rules and was queued. Its outcome arrives as message.sent, message.delivered or message.failed, and GET /v0/messages/{message_id} reads its status. The meaning of a provider’s delivery state should remain explicit in your integration.
How do I get started with Flow?
Get a test key in one call and follow the Messaging API quickstart, or start a project with the Flow Framework quickstart. Sign up for the free trial on Flow’s shared lines, or talk to us about your channels and API requirements.
