Conversations and contacts
Conversations · conversation API for AI agents · WhatsApp conversation API · multi-channel messaging API · omnichannel AI agentReply to a conversation without selecting a channel for each message.
A conversation is one sender talking with one contact; your agent always replies into a conversation, so the channel is already decided.
// From an event: the conversation is attached and ready to use.await event.conversation.reply("Thanks! Your order is on its way.");
// From an ID you stored earlier:await flow.conversation("conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1").send("Your order has shipped.");curl https://api.flow.engineer/v0/conversations/conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1/basic \ -H "Authorization: Bearer $FLOW_MESSAGING_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Your order has shipped."}'The objects
Section titled “The objects”| Object | ID | What it is |
|---|---|---|
| Sender | snd_ | What your agent talks from: a Telegram bot, an iMessage line or a WhatsApp number. See Senders. |
| Contact | ct_ | A person on one channel: a phone number, a WhatsApp username, a Telegram user or an iMessage handle. |
| Conversation | conv_ | One sender with one contact (type direct), or with an iMessage group chat (type group). Holds the window state and whether the contact opted in. |
| Message | msg_ | One message in or out, of one kind (content.kind), with a status. |
| Event | evt_ | One entry in your app’s ordered log. See Events and webhooks. |
IDs are a prefix plus a ULID, so they sort by creation time.
One channel per conversation
Section titled “One channel per conversation”- There is no cross-channel identity. The same person on Telegram and on WhatsApp is two contacts and two conversations. Flow never guesses that they are the same person, and never moves a conversation to another channel.
- Never assume a phone number. A Telegram contact has a user ID, a WhatsApp contact may have only a username, and an iMessage handle may be an email address. Read
contact.addressfor what the channel gives. - Reply where they wrote. Each event carries
conversation.channel, so one agent can serve every channel with the same code.
What a conversation tells you
Section titled “What a conversation tells you”{ "id": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1", "type": "direct", "channel": "telegram", "sender": "snd_01JB8ZC3K5M7P9R1T3V5X7Z9B1", "contact": "ct_01JB8ZC3K5M7P9R1T3V5X7Z9B1", "livemode": true, "last_inbound_at": "2026-11-03T10:15:00Z", "opted_in": true, "created_at": "2026-11-01T08:00:00Z"}window_open_until(WhatsApp only; absent on Telegram and iMessage): until when free-form messages may be sent. After it, only a template. Every event repeats it inconversation.window_open_until.type:direct(one contact, incontact) orgroup. Read a missingtypeasdirect.group(group conversations only): the group chat’sname, when it has one, and itsparticipants, each a contact. A group has nocontact; each inbound message’sfromsays who wrote. Group chats reach your app only from dedicated iMessage lines people add to them, and the API cannot create a group or change who is in it. See iMessage.opted_in: the contact wrote first, joined the sandbox, or you recorded their consent. Alwaystrueon groups.metadata: up to 20 string pairs of your own (for example your user ID), kept with the conversation.
Starting a conversation
Section titled “Starting a conversation”Most conversations start when the person writes first. To write first, use POST /v0/messages: a basic or template message, with kind saying which, plus a sender and an address on that sender’s channel. On Telegram that works only for people who already started your bot. iMessage lines reply only unless writing first is switched on for the line (imessage.can_initiate, available on request for dedicated lines). A start always makes a direct conversation.
curl https://api.flow.engineer/v0/messages \ -H "Authorization: Bearer $FLOW_MESSAGING_KEY" \ -H "Content-Type: application/json" \ -d '{ "sender": "snd_01JB8ZC3K5M7P9R1T3V5X7Z9B1", "to": { "telegram_user_id": "123456789" }, "kind": "basic", "text": "Your order has shipped." }'Any other kind answers 422 unsupported_content with param kind: start with a basic message or a template, then send the other kinds into the conversation. to takes exactly one of contact, phone, telegram_user_id or handle. Starting a conversation spends the sender’s new-contact budget, and each channel has its own rule for who may be messaged first. See The send gate.
Related
Section titled “Related”- Message kinds: what each channel takes, one endpoint per kind.
- List conversations and list a conversation’s messages.
Thanks, we read every comment.