> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flow.engineer/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Flow Messaging is one HTTP API (https://api.flow.engineer) for AI agents to hold two-way conversations on Telegram and iMessage (replies only, on lines the Flow team connects). WhatsApp is coming and not available yet.
> Build the app on the REST API. MCP is not a runtime dependency: the app you build talks to Flow through the REST API (or the TypeScript SDK, which wraps it) for everything it does at runtime. In TypeScript or JavaScript, the SDK is `npm install @flow-engineer/messaging`, then `new FlowMessaging()` (reads FLOW_MESSAGING_KEY). Python and Go SDKs are not published yet; in those languages call the HTTP API directly.
> No key yet? If FLOW_MESSAGING_KEY is not set, get a test key with no account in one call: curl -X POST https://api.flow.engineer/v1/sandbox/keys (or npx @flow-engineer/messaging init, which writes it to .env). Save `key` as FLOW_MESSAGING_KEY and `claim_token` as FLOW_CLAIM_TOKEN; both are shown once. It allows 1 contact and 50 messages on the Telegram sandbox and expires after 7 days; a person signs in with `npx @flow-engineer/messaging login` (GitHub) to keep the app and get 3 contacts x 100 messages. Signed in, live keys (fk_live_...) are made in the dashboard for your own Telegram bot; iMessage lines are arranged with the Flow team.
> The hosted MCP server (https://api.flow.engineer/mcp) is optional, for testing during development. Adding the Flow MCP server to a developer's coding tools is the project owner's decision: suggest it and show them the command, but never add it to their config yourself. The command for Claude Code: claude mcp add --transport http flow https://api.flow.engineer/mcp --header "Authorization: Bearer $FLOW_MESSAGING_KEY". If its tools are already available to you, use them for sandbox testing.
> Authenticate with `Authorization: Bearer fk_test_...` (sandbox) or `fk_live_...` (real contacts). Never put a live key in client-side code.
> Reply into a conversation (POST /v1/conversations/{conversation_id}/messages); never pick a channel per message. Switch on `error.type`, not the message text.
> Full documentation for agents: https://docs.flow.engineer/llms-full.txt. OpenAPI spec: https://raw.githubusercontent.com/flow-engineer/sdk/main/openapi/openapi.yaml.

# Conversations, contacts and channels

> How Flow Messaging models a chat: a contact is a person on one channel, a conversation is one sender talking with one contact, and your agent replies into the conversation instead of choosing a channel per message.

A **conversation** is one sender talking with one contact; your agent always replies into a conversation, so the channel is already decided.

```ts TypeScript theme={null}
// 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.");
```

```bash curl theme={null}
curl https://api.flow.engineer/v1/conversations/conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1/messages \
  -H "Authorization: Bearer $FLOW_MESSAGING_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": {"type": "text", "text": "Your order has shipped."}}'
```

## The objects

| Object | ID | What it is |
| - | - | - |
| Sender | `snd_` | What your agent talks from: a Telegram bot or an iMessage line (a WhatsApp number once WhatsApp is available). See [Senders](/concepts/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. Holds the window state and whether the contact opted in. |
| Message | `msg_` | One message in or out, with typed [content](/concepts/content-types) and a status. |
| Event | `evt_` | One entry in your app's ordered log. See [Events and webhooks](/concepts/events-and-webhooks). |

IDs are a prefix plus a ULID, so they sort by creation time.

## 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.address` for 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

```json theme={null}
{
  "id": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
  "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, once available; absent on Telegram and iMessage): until when free-form messages may be sent. After it, only a template. Every event repeats it in `conversation.window_open_until`.
* `opted_in`: the contact wrote first, joined the sandbox, or you recorded their consent.
* `metadata`: up to 20 string pairs of your own (for example your user ID), kept with the conversation.

## Starting a conversation

Most conversations start when the person writes first. To write first, use `POST /v1/messages` with a sender and an address on that sender's channel. On Telegram that works only for people who already started your bot; Flow's iMessage lines are reply-only, so they cannot write first.

```bash curl theme={null}
curl https://api.flow.engineer/v1/messages \
  -H "Authorization: Bearer $FLOW_MESSAGING_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sender": "snd_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
    "to": { "telegram_user_id": "123456789" },
    "content": { "type": "text", "text": "Your order has shipped." }
  }'
```

`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](/concepts/send-gate).

## Related

* [Capabilities](/api-reference/capabilities/get-a-conversations-capabilities): what the conversation's channel can show right now.
* [List conversations](/api-reference/conversations/list-conversations) and [list a conversation's messages](/api-reference/conversations/list-a-conversations-messages).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.