> ## 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.

# Skill

> 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 dedicated lines are set up by the Flow team; it replies after the person writes first (writing first is available on request for dedicated lines), 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 Senders page; 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 the Flow team registers for a customer; not generally available. There is no voice calling. To ask the Flow team for a number or line, about current pricing and account terms or for support, write to [samyak@flow.engineer](mailto:samyak@flow.engineer) or [prabal@flow.engineer](mailto:prabal@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 (dedicated iMessage lines and SMS numbers are arranged with the Flow team).

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 `Hi Sandbox app (ref wild-otter-04508705)`) to the line. The trial has room for 25 people per channel in total, over 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}`). Otherwise the Flow team can grant an account the line. The shared line replies only (the person writes first) and ignores group chats. Dedicated iMessage lines are set up by the Flow team; writing first (`sender.imessage.can_initiate`) is available on request, on dedicated lines only.
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 `Hi Sandbox app (ref wild-otter-04508705)`, to it. The join then reaches the app as the person's first `message.received` (`Hi 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 on Flow's shared Telegram, WhatsApp and iMessage senders, over all the account's apps, 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). Signed-in users manage keys at [https://api.flow.engineer/admin](https://api.flow.engineer/admin). Details: [https://docs.flow.engineer/unified-messaging-api/get-a-key.md](https://docs.flow.engineer/unified-messaging-api/get-a-key.md)

## Receive and reply (TypeScript)

```ts theme={null}
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.
* 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/observe](https://flow.engineer/observe)) 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 on Observe's Setup page at [https://flow.engineer/observe](https://flow.engineer/observe) 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 ([samyak@flow.engineer](mailto:samyak@flow.engineer) or [prabal@flow.engineer](mailto:prabal@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://docs.flow.engineer/unified-messaging-api/observe/traces.md](https://docs.flow.engineer/unified-messaging-api/observe/traces.md). Frameworks: [https://docs.flow.engineer/unified-messaging-api/observe/frameworks.md](https://docs.flow.engineer/unified-messaging-api/observe/frameworks.md)
* Before shipping: play test chats (simulated contacts on Flow Messaging, synthetic turns elsewhere) and read the verdicts. [https://docs.flow.engineer/unified-messaging-api/trust.md](https://docs.flow.engineer/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 theme={null}
claude mcp add --transport http flow https://api.flow.engineer/mcp --header "Authorization: Bearer $FLOW_MESSAGING_KEY"
```

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

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

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

```json theme={null}
{"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

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


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