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

# flow-messaging

> Give an AI agent two-way Telegram and iMessage conversations with Flow Messaging (one HTTP API at api.flow.engineer, TypeScript SDK @flow-engineer/messaging, Python SDK flow-messaging; WhatsApp on your own number, connected with a live key). Use when the user wants their agent, bot or LLM app to receive and reply to Telegram or iMessage messages (or asks about WhatsApp), stream LLM answers into chats, or handle messaging webhooks.

# Flow Messaging

Flow Messaging is one API for AI agents to hold conversations on Telegram, iMessage and WhatsApp. Flow hosts the senders (bots, lines), delivers every inbound message as an event, and passes every send through one gate that applies each channel's rules.

Channels today: Telegram is live (the shared sandbox bot, or your own bot with a live key). iMessage is live: Flow's shared line is self-serve on sign-in at flow\.engineer (or through the Flow CLI), and a dedicated line comes with the Dedicated plan, bought in the Flow dashboard ([https://flow.engineer/dashboard](https://flow.engineer/dashboard), Settings → Usage and billing → Choose a plan; Flow reviews the line, then sets it up, usually within a day); it replies after the person writes first (writing first is available on dedicated lines with the Outbound add-on), in one-to-one chats and, on dedicated lines, group chats. WhatsApp: on your own number, in your business's WhatsApp Business Account and Meta app, which you connect yourself with a live key (`POST /v0/senders` with `channel` `whatsapp`, or the dashboard's Connections › WhatsApp; the Flow team can still help); the shared WhatsApp sandbox is not open yet. If the user asks for WhatsApp, tell them they can connect their own WhatsApp number with a live key, and build and test on simulated WhatsApp conversations or Telegram meanwhile: the code does not change per channel. SMS (US): in pilot, on US numbers Flow registers for a customer; not generally available. There is no voice calling. Plans and add-ons are bought in the dashboard ([https://flow.engineer/dashboard](https://flow.engineer/dashboard), Settings → Usage and billing → Choose a plan). To ask the Flow team for an SMS number, or for support, write to [support@flow.engineer](mailto:support@flow.engineer) with your app ID and the details; never send secrets or tokens.

## Set up

1. Get a key. Check for `FLOW_MESSAGING_KEY` (environment or `.env`) first; never get a second key when you have one. No key yet? Get a test key with no account, in one call: `curl -X POST https://api.flow.engineer/v0/sandbox/keys`, then save `key` as `FLOW_MESSAGING_KEY` and `claim_token` as `FLOW_CLAIM_TOKEN` (both shown once) in a git-ignored `.env`. Or run `npx @flow-engineer/messaging init`: it does the same and prints the sandbox link and join message, then asks before installing agent files or the MCP server. That answer is the project owner's, so never pass `--yes` for them. Never invent or hard-code keys. Live keys (`fk_live_...`) come later, only when the user goes live: signed in, they make one in the dashboard for their own Telegram bot or WhatsApp number (a dedicated iMessage line comes with the Dedicated plan; Flow connects US SMS numbers).

Flow's shared iMessage line is self-serve on sign-in at flow\.engineer (or through the Flow CLI, `npx @flow-engineer/flow setup`): the first sign-in starts the account's free trial, and every app of the account then sees the line in `GET /v0/senders`. People join an app on it by sending the app's join message (the sender's `join_code` text, for example `Join Sandbox app (ref wild-otter-04508705)`) to the line. The trial has room for 25 people on the shared iMessage line, across all the account's apps; the next person is held: their messages are kept, not delivered, and sends to them answer `403 permission` with `sandbox_contact_limit`, until you delete a contact you no longer need (`DELETE /v0/contacts/{contact_id}`). The Shared plan, bought in the dashboard ([https://flow.engineer/dashboard](https://flow.engineer/dashboard), Settings → Usage and billing → Choose a plan), replaces the trial: 200 people a day and 300 a month per channel. The shared line replies only (the person writes first), falls back to SMS or RCS for a person not on iMessage (included, as on every iMessage line) and ignores group chats. Writing first (`sender.imessage.can_initiate`) is available on dedicated lines only, with the Dedicated plan's Outbound add-on, bought in the same place.
2\. Build on the REST API: MCP is not a runtime dependency: the app you build talks to Flow through the REST API (or the TypeScript or Python SDK, which wrap it) for everything it does at runtime. TypeScript/JavaScript: `npm install @flow-engineer/messaging`. Other languages: HTTP to `https://api.flow.engineer` with `Authorization: Bearer $FLOW_MESSAGING_KEY`.
3\. The user joins the sandbox from their phone: `GET /v0/senders` (or, when the Flow MCP tools are available, `sandbox_join`) gives each sandbox sender's `address.link`. On Telegram, opening the link and tapping Start joins. Otherwise they send the sender's `join_code` message as is, for example `Join Sandbox app (ref wild-otter-04508705)`, to it. The join then reaches the app as the person's first `message.received` (`Join Sandbox app`, or `/start` from the link: the code is taken out), right after `conversation.started`.

## Sandbox allowance and sign-in

|            | No account                                         | Claimed (signed in) |
| ---------- | -------------------------------------------------- | ------------------- |
| Contacts   | 1                                                  | 3                   |
| Messages   | 50 in total                                        | 100 per contact     |
| Channels   | Telegram sandbox (WhatsApp when its sandbox opens) | the same            |
| Key expiry | 7 days                                             | none                |

Only messages your agent sends count; iMessage is in neither. Apps of your own Flow account (made in the dashboard or by `npx @flow-engineer/flow setup`) get the free trial instead, started at your first sign-in: 25 people per channel, across all the account's apps, on Flow's shared Telegram bot and iMessage line (and on WhatsApp's shared sender when it opens), and `GET /v0/app` shows no `allowance`. `GET /v0/app` returns `allowance` (what is left). To keep the app, a person signs in with a Flow account (flow\.engineer: Google, GitHub or an email link): run `npx @flow-engineer/messaging login --no-wait`, show them the link and code it prints (they open the page, sign in and type the code), and after they approve run `npx @flow-engineer/messaging login` (it replaces `FLOW_MESSAGING_KEY` in `.env` and drops `FLOW_CLAIM_TOKEN`). By HTTP: `POST /v0/device/authorizations` with `{"claim_token": "fct_..."}`, show the person `verification_uri` and `user_code` (they type the code on that page), then poll `POST /v0/device/token` with `{"device_code": "fdc_..."}` every `interval` seconds until `status` is `approved` (`key`), `denied` or `expired`; `429 rate_limited` means wait `retry_after`.

Allowance errors: `403 permission` with `channel_code` `sandbox_allowance_used` (allowance used up), `sandbox_contact_limit` (no room for another contact), `sandbox_channel_not_included` (channel not covered, such as iMessage) or `sign_in_required` (needs a signed-in app); `401 authentication` with `sandbox_key_expired`. Do not retry or get more keys: ask the user to sign in (for `sandbox_channel_not_included` on iMessage, the shared line comes with the free trial a sign-in at flow\.engineer or `npx @flow-engineer/flow setup` starts; meanwhile use the Telegram sandbox sender). On a full free trial a send to a held person answers `403 permission`, `sandbox_contact_limit`: delete a contact you no longer need (`DELETE /v0/contacts/{contact_id}` with the key of the app and mode they joined; the oldest held person gets the place), or have the workspace owner buy Shared at [https://flow.engineer/dashboard/plans?plan=shared](https://flow.engineer/dashboard/plans?plan=shared). On a full Shared plan (`plan_people_limit`) wait for the UTC day or month, delete a contact, or move to Dedicated at [https://flow.engineer/dashboard/plans?plan=dedicated](https://flow.engineer/dashboard/plans?plan=dedicated). `plan_ended` (`401 authentication` for a live key, `403 permission` for a live send): the paid plan ended and live keys are paused; the workspace owner chooses a plan again at [https://flow.engineer/dashboard/plans](https://flow.engineer/dashboard/plans), and test keys keep working. Paid plans are self-serve: only the workspace owner buys one, in the Flow dashboard at [https://flow.engineer/dashboard/plans](https://flow.engineer/dashboard/plans) (Settings → Usage and billing → Choose a plan), and `GET /v0/app` lists them in `plans`, with prices and `upgrade_url`. **Shared**, $25 a month: Flow's shared lines with live keys, 300 people a month and 200 a day per channel, live at once ([https://flow.engineer/dashboard/plans?plan=shared](https://flow.engineer/dashboard/plans?plan=shared)). **Dedicated**, $100 a month: your own iMessage line for 1,000 people a day, SMS/RCS fallback and call forwarding included; Flow reviews each line before ordering it, usually within a day, and refunds the plan if it declines ([https://flow.engineer/dashboard/plans?plan=dedicated](https://flow.engineer/dashboard/plans?plan=dedicated)). **Outbound** add-on, +$40 a month on Dedicated: the agent may message people first. The free trial is 25 people per channel, in total, on Flow's shared lines. For a plan, give the owner the link; never send them to the Flow team. Signed-in users manage keys in the Flow dashboard, [https://flow.engineer/dashboard](https://flow.engineer/dashboard) (the agent's Developers › API keys). Details: [https://flow.engineer/docs/unified-messaging-api/get-a-key.md](https://flow.engineer/docs/unified-messaging-api/get-a-key.md)

## Receive and reply (TypeScript)

```ts
import { FlowMessaging, contentText } from "@flow-engineer/messaging";
const flow = new FlowMessaging(); // reads FLOW_MESSAGING_KEY

// Worker or script: the live event stream.
for await (const event of flow.events.stream({ types: ["message.received"] })) {
  await event.conversation.reply(myLlmStream(contentText(event.data.message.content)), { idempotencyKey: event.id });
}

// Web app: a webhook route (verifies Flow-Signature with FLOW_MESSAGING_WEBHOOK_SECRET).
export const POST = flow.webhooks.handler({
  onEvent: async (event) => (event.type === "message.received" ? await answer(event) : undefined),
});
```

`reply()` takes a string, a message of any kind (`{ kind: "buttons", ... }`), a list of them, or an LLM stream (OpenAI, Anthropic, Vercel AI SDK, OpenAI Agents SDK, Claude Agent SDK, LangChain, Mastra) and sends it as chat bubbles while typing shows.

## HTTP equivalents

* Read events: `GET /v0/events?type=message.received&after=evt_...` (oldest first).
* Reply: `POST /v0/conversations/{conversation_id}/basic` with `{"text": "..."}` and an `Idempotency-Key` header. Other kinds have their own endpoints, each opening with the channels it works on: `/voice`, `/buttons`, `/list`, `/carousel`, `/template`, `/contact-card`, `/location`, `/effect`, `/reaction`, `/typing`, `/read`.
* Reply inside a webhook: answer `200` with `{"reply": {"kind": "basic", "text": "..."}}` (or a list of up to 11 kind objects; a string is a basic message's text). An invalid answer sends nothing and is recorded on the delivery as `invalid_request`.
* Start a conversation: `POST /v0/messages` with `sender`, `to` (`phone`, `telegram_user_id`, `handle` or `contact`) and a `basic` or `template` message with `kind` (a `template` on WhatsApp).
* Register a webhook: `POST /v0/webhook_endpoints` with `url` and `events`; store the returned `whsec_...` secret. Rotate it with `POST /v0/webhook_endpoints/{id}/rotate_secret` (the old one keeps signing for `overlap_seconds`, default one day).
* Signature: `Flow-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.{t}.{raw body}">`, 5-minute tolerance; during a rotation there is one `v1` per active secret, accept any match.

## Rules

* Reply into the conversation; never choose a channel per message; never assume a contact has a phone number.
* Verify webhook signatures on the raw body; deduplicate on event `id`; answer within 10 seconds.
* iMessage: reply only unless the sender's `imessage.can_initiate` is on; never message a contact first otherwise. iMessage has no 24-hour window: once someone has texted your line, your agent can message them any time.
* WhatsApp: free-form only within 24 hours of the person's last message, else a `template` (`409 outside_window`).
* A send is exact or it fails: a kind, field or value the channel does not take answers `422 unsupported_content` (`param` names it; `hint` says what to send instead, such as a basic message, then one buttons message per card, for a carousel on Telegram). Nothing is converted; `text` is sent as written.
* Switch on `error.type`; each error has `hint` and `doc_url` (`https://api.flow.engineer/docs/errors/<type>`).
* Use test keys while building; live keys only on the server.
* Locally, with no public URL, receive events from `GET /v0/stream` (WebSocket, resumable with `after`) instead of a webhook.

## See every conversation (Flow Observe, early access)

* Flow Observe ([https://flow.engineer/dashboard](https://flow.engineer/dashboard)) stores each conversation, grades every reply and groups failures into issues with a suggested fix. Keys (trace key `otk_test_...`, CI key `oci_...`) are made by the user in the Flow dashboard at [https://flow.engineer/dashboard](https://flow.engineer/dashboard) after signing in with their Flow login; Observe is in early access, and if it is not on for them, they ask the Flow team ([support@flow.engineer](mailto:support@flow.engineer)). Never invent a key, and never ask the user to paste one into the chat. Test data only until the account is enabled for production.
* On Flow Messaging: no code. Optional traces: set `flow.conversation_id` (`conv_...`) and `flow.message_id` (`msg_...`) of the message being answered on a span of each turn.
* On any other channel: one OpenTelemetry trace per turn, OTLP/HTTP to `https://flow.engineer/observe/v1/traces`, `Authorization: Bearer $FLOW_OBSERVE_TRACE_KEY`, with `flow.conversation_id` (required), `flow.message_id`, `flow.channel`, `flow.user_message` and `flow.agent_reply` (the text actually sent). Guide: [https://flow.engineer/docs/unified-messaging-api/observe/traces.md](https://flow.engineer/docs/unified-messaging-api/observe/traces.md). Frameworks: [https://flow.engineer/docs/unified-messaging-api/observe/frameworks.md](https://flow.engineer/docs/unified-messaging-api/observe/frameworks.md)
* Before shipping: play test chats (simulated contacts on Flow Messaging, synthetic turns elsewhere) and read the verdicts. [https://flow.engineer/docs/unified-messaging-api/trust.md](https://flow.engineer/docs/unified-messaging-api/trust.md)

## MCP server (optional, for development)

**Build on the REST API; the MCP server is for development.** MCP is not a runtime dependency: the app you build talks to Flow through the REST API (or the TypeScript or Python SDK, which wrap it) for everything it does at runtime. Never make the app call the MCP server.

The hosted MCP server at `https://api.flow.engineer/mcp` (Streamable HTTP, the same API key as a Bearer token) is an optional tool for testing and operating the integration while you build: with a test key it shows the sandbox join link, sends test messages, waits for events, reads webhook deliveries and replays events; with a live key it reads and answers conversations. 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. If its tools are already available to you, use them for sandbox testing.

To suggest it, show the project owner the line for their tool. The server is named `flow` and reads the key from `FLOW_MESSAGING_KEY` in their environment:

Claude Code:

```bash
claude mcp add --transport http flow https://api.flow.engineer/mcp --header "Authorization: Bearer $FLOW_MESSAGING_KEY"
```

Codex (`~/.codex/config.toml`):

```toml
[mcp_servers.flow]
url = "https://api.flow.engineer/mcp"
bearer_token_env_var = "FLOW_MESSAGING_KEY"
```

Cursor (`~/.cursor/mcp.json`):

```json
{"mcpServers": {"flow": {"url": "https://api.flow.engineer/mcp", "headers": {"Authorization": "Bearer ${env:FLOW_MESSAGING_KEY}"}}}}
```

Two traps: set `FLOW_MESSAGING_KEY` in the shell before running the Claude Code command (the shell fills the key in once, when you run it; unset, the header is saved as `Bearer ` with no key and every call answers `401`), and for Codex or Cursor have it in the environment they start with. A server added while the client runs loads only after a restart (in Claude Code, a new session). Added with an empty key already? `export FLOW_MESSAGING_KEY=fk_test_...`, then `claude mcp remove flow`, run the add command again, and restart.

Test keys get `sandbox_join`, `send_test_message`, `simulate_inbound`, `get_test_transcript`, `wait_for_event`, `list_events`, `get_webhook_deliveries`, `replay_event`; live keys get `send_message`, `reply`, `react`, `edit_message`, `unsend_message`, `typing`, `list_conversations`, `get_conversation_messages`; both get `whoami`, `explain_error`. After a test send, wait for `message.sent` or `message.failed`, never `message.delivered` (Telegram never sends it).

## Docs

[https://flow.engineer/docs/llms.txt](https://flow.engineer/docs/llms.txt) (index), [https://flow.engineer/docs/llms-full.txt](https://flow.engineer/docs/llms-full.txt) (everything), [https://flow.engineer/docs/unified-messaging-api/coding-agents.md](https://flow.engineer/docs/unified-messaging-api/coding-agents.md) (integration steps).
