> ## 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), and on WhatsApp numbers the Flow team connects. If the user asks for WhatsApp, tell them WhatsApp numbers are arranged with the Flow team, and build and test on Telegram meanwhile: the code does not change per channel.
> 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" (run it with FLOW_MESSAGING_KEY set, then restart the client). 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.

# Recipes: ask and wait for a reply, many bots, serverless, broadcasts, GDPR and more

> Short answers for common tasks with Flow Messaging: a program that asks and waits for the reply, many Telegram bots, webhooks on serverless, files and voice notes, WhatsApp reminders, moving a polling bot, broadcasts, deleting a contact, a leaked key, going to production, one bot per customer, delivery guarantees, tests in CI and team access.

The same recipes are in the agent quickstart (section 14), which coding agents read at `https://api.flow.engineer/docs/quickstart.md`.

## Ask, wait for the reply, continue

A program that asks a person one thing on Telegram, waits for the answer and goes on (a CLI, a script, a test). Four rules make it reliable:

1. **Open the stream first.** `GET /v1/stream` without `after` starts at the moment it opens: anything earlier is not replayed. Once it is open, nothing later is missed.
2. **Then look for a join that already happened.** A person who joined before the program started is already a conversation: `GET /v1/conversations?channel=telegram` (newest first). Skip `simulated: true` ones. Only when there is none, show the join link and wait for `conversation.started`.
3. **Confirm the send.** The `202` only means queued: wait for `message.sent` or `message.failed` with that message's `id` (Telegram never sends `message.delivered`).
4. **Wait for the reply with a timeout.** The next `message.received` in that conversation is the answer; give up after a time you choose and tell the person.

```ts theme={null}
// ask.mts: Node 22+ (ESM, top-level await), no dependencies. FLOW_MESSAGING_KEY is a test key (or live, with your own bot).
const BASE = "https://api.flow.engineer";
const KEY = process.env.FLOW_MESSAGING_KEY!;
const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };
type Init = { method?: string; headers?: Record<string, string>; body?: string };
const api = async (path: string, init: Init = {}) => {
  const res = await fetch(BASE + path, { ...init, headers: { ...H, ...init.headers } });
  const body = await res.json();
  if (!res.ok) throw new Error(`${body.error.type}: ${body.error.hint}`); // hint says what to change
  return body;
};

// 1. Stream first. Events wait in `inbox` until something takes them.
const inbox: any[] = [];
let wake = () => {};
const ws = new WebSocket(`wss://api.flow.engineer/v1/stream?type=conversation.started` +
  `&type=message.received&type=message.sent&type=message.failed`, ["flow", "flow.key." + KEY]);
ws.onmessage = (m) => {
  const f = JSON.parse(String(m.data));
  if (f.type === "event") { inbox.push(f.event); wake(); }
  else if (f.type === "error") console.error(f.error.type, f.error.hint); // then it closes (4401: bad key)
};
await new Promise((ok, fail) => { ws.onopen = ok; ws.onerror = fail; });

function next(match: (ev: any) => boolean, ms: number): Promise<any> {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error(`no event within ${ms / 1000}s`)), ms);
    wake = () => {
      const i = inbox.findIndex(match);
      if (i >= 0) { clearTimeout(timer); resolve(inbox.splice(i, 1)[0]); }
    };
    wake();
  });
}

// 2. A join from before the program started is already a conversation.
const convs = await api("/v1/conversations?channel=telegram&limit=20");
let conv: string | undefined = convs.data.find((c: any) => !c.simulated)?.id;
if (!conv) {
  const bots = await api("/v1/senders?channel=telegram");
  console.log(`Open ${bots.data[0].address.link} on your phone and tap Start.`);
  conv = (await next((e) => e.type === "conversation.started" && !e.conversation.simulated, 10 * 60_000)).conversation.id;
}

// 3 and 4. Ask, confirm the send, wait for the answer.
async function ask(text: string, timeoutMs = 5 * 60_000): Promise<string | null> {
  inbox.splice(0); // what the person wrote before the question is not its answer
  const msg = await api(`/v1/conversations/${conv}/messages`, {
    method: "POST", headers: { "Idempotency-Key": crypto.randomUUID() },
    body: JSON.stringify({ content: { type: "text", text } }),
  });
  const outcome = await next((e) => e.data?.message?.id === msg.id &&
    (e.type === "message.sent" || e.type === "message.failed"), 60_000);
  if (outcome.type === "message.failed") {
    const err = outcome.data.message.error;
    throw new Error(`send failed: ${err.type} ${err.channel_code ?? ""}: ${err.hint}`);
  }
  try {
    const ev = await next((e) => e.type === "message.received" && e.conversation.id === conv, timeoutMs);
    const c = ev.data.message.content;
    return c.type === "text" ? c.text : c.type === "button_reply" ? c.label : `[${c.type}]`;
  } catch {
    return null; // timed out: say so, or ask again later
  }
}

const answer = await ask("Which city should I check the weather for?");
console.log(answer ?? "No answer in time.");
ws.close();
```

* **Long waits**: the stream ends after 24 hours or on a server restart with a `reconnect` frame. Reconnect with `after` set to the last event `id` you saw: it replays what you missed, then goes live.
* **No WebSocket** (serverless, cron): poll the event log instead, `GET /v1/events?conversation=conv_...&type=message.received&type=message.sent&type=message.failed&after=evt_...` (oldest first; `after` is the last event you handled; page while `has_more`). Or register a webhook and keep the question's state in your store.
* **In tests without a phone**, post the person's answer with `POST /v1/test/conversations/{conversation_id}/inbound` on a simulated conversation (`POST /v1/test/conversations`): free, and it arrives as the same `message.received`.

### Many Telegram bots (one per customer, brand or language)

* A person creates each bot in Telegram's @BotFather (`/newbot`, a name, a username ending in `bot`); an agent cannot. Ask them: "Create the bot in @BotFather and put its token in `.env` (or your secret store) as `TELEGRAM_BOT_TOKEN_<NAME>`; don't paste it here." Never ask for the token in chat.
* With a live key, `POST /v1/senders {"channel":"telegram","telegram_bot_token":"..."}` once per bot (`Idempotency-Key` per bot). One bot is one sender; connecting it again updates that sender; connecting it to another of your apps moves it there. There is no limit on bots per app.
* Send from a bot with `"sender": "snd_..."` on `POST /v1/messages`; replies go into the conversation and leave from its bot. Every event names its `conversation.sender`, so route by sender.

### Serverless (Vercel, Workers, Lambda): webhooks only

* Register `https://<your-app>/api/flow` once with `POST /v1/webhook_endpoints` and store the `whsec_...` secret as `FLOW_MESSAGING_WEBHOOK_SECRET`. Verify `Flow-Signature` on the raw body (in TypeScript `flow.webhooks.handler(...)` does it), dedupe on `event.id`, answer within 10 seconds.
* Fast answer: return `{"reply": ...}` in the webhook answer. Slow agent: answer `200 {}` at once, then reply with `POST /v1/conversations/{conversation_id}/messages` (on Vercel inside `waitUntil`). Keep conversation state in your database, keyed by `conversation.id`.
* Missed deliveries are retried for 3 days; to catch up, read `GET /v1/events?after=evt_...` from a cron job.
* Previews and local runs: Flow calls only public HTTPS URLs. Locally, no tunnel needed: `npx @flow-engineer/messaging listen --forward-to http://localhost:3000/api/flow` reads the stream and `POST`s each event to your handler, signed like a real delivery with `FLOW_MESSAGING_WEBHOOK_SECRET`.

### Add Flow to the IDE (MCP)

The hosted MCP server is an optional development tool. Adding it is the project owner's decision: show them the command (section 12 of the quickstart, or `docs/mcp`), never add it yourself. The app itself always uses the REST API.

### Send a PDF, receive a voice note

* Uploads (`POST /v1/files`, multipart, up to 100 MB) take images, audio, video and plain text. **Documents and archives (PDF, Office, zip) are refused with `422 unsupported_content`** until uploads can be malware-scanned. To send a PDF today, host it yourself and send `{"type":"media","kind":"document","url":"https://..."}` (Telegram fetches it, up to 20 MB), or send a link as text.
* A voice note arrives as `message.received` with `{"type":"voice","file_id":"file_...","url":"...","duration_seconds":4}`. Download it with `GET /v1/files/{file_id}` (your API key; it answers `302` to a short-lived signed URL, so follow redirects). Send one with `{"type":"voice","file_id":"file_..."}` (OGG/Opus on Telegram).

### WhatsApp: a reminder the next day

Flow has no scheduled sends: schedule in your own app (cron, queue) and send then. Next day is usually outside WhatsApp's 24-hour window, so only an approved `template` goes (`POST /v1/templates` on your WhatsApp sender with a live key; else `409 outside_window`). Test it now on a simulated WhatsApp conversation: templates on the simulated sender are approved at once. WhatsApp numbers are connected by the Flow team.

### Move a live polling bot to Flow

Connect its token with a live key (`POST /v1/senders`). Flow sets the bot's webhook, which ends `getUpdates` polling: stop the old poller first (Telegram answers its next `getUpdates` with `409 Conflict`). Updates Telegram still holds are delivered to Flow, not dropped. Conversations start fresh in Flow (it has no history from before); people do not need to press Start again. To go back, `DELETE /v1/senders/{sender_id}` removes the webhook.

### Broadcast to many people

* There is no bulk endpoint: one `POST /v1/messages` per person, each with its own `Idempotency-Key` (for example `campaign-7-ct_...`), from a queue of your own.
* Telegram: a bot can message only people who started it, so a broadcast goes to people who wrote to your bot. Each bot sends about 20 messages a second (burst 40); faster answers `429 rate_limited` with `Retry-After`. The per-key request limit is 600 a minute on the free plan (`RateLimit-*` headers), so 5,000 messages take about 9 minutes.
* New conversations per sender are budgeted: the sender's `limits` (`new_contacts_per_day`, `new_contacts_per_hour`) show today's numbers; past them a start answers `429 new_contact_limit` with `retry_after`: queue it, do not loop. iMessage lines start no cold conversations; WhatsApp needs a template and Meta's tier caps new contacts a day; SMS needs consent.
* The same first message to 20 new contacts within an hour throttles a dedicated sender for 24 hours (`429 sender_throttled` on starts; replies still go). Personalise first messages; templates and media without a caption do not count. Limits and signals: quickstart section 10.
* Every send still ends `message.sent` or `message.failed`: count those, not the `202`s.

### Delete a person's data (GDPR)

`DELETE /v1/contacts/{contact_id}` (`204`, repeat-safe) erases the contact's name and address, all their message content and files, their events' payloads, pending deliveries and queued sends at once, and emits `contact.deleted`. IDs and timestamps stay. Find the contact ID on their events (`conversation.contact`) or `GET /v1/contacts`; there is no lookup by phone number or username. Content is otherwise kept 30 days.

Erasure removes Flow's copy only: messages already delivered stay on the person's phone. To remove those too, unsend them first (`DELETE /v1/messages/{message_id}`: Telegram within 48 hours, iMessage within 2 minutes; WhatsApp and SMS cannot), wait until each shows `status` `unsent`, then erase. After erasure nothing can be unsent.

### A leaked API key

1. Make a new key: dashboard ([https://api.flow.engineer/admin/keys](https://api.flow.engineer/admin/keys), `?mode=live` for live keys) > **Create live key** (or test). Put it where the old one was and deploy.
2. Revoke the old one: Keys > **Revoke** on the key ending in its last four characters. It stops working at once; open streams close within a minute (`4401`).
3. Read `GET /v1/events` for sends you did not make. A leaked webhook secret: `POST /v1/webhook_endpoints/{webhook_endpoint_id}/rotate_secret`. A leaked bot token: revoke it in @BotFather and connect the new one (`POST /v1/senders`, same sender).

An app made without an account has no dashboard: get a new sandbox key, or sign in first.

### Prototype to production

1. Sign in to keep the app (`npx @flow-engineer/messaging login`): the app, its data and webhooks stay.
2. Dashboard > Live > **Create live key**; keep it apart from the test key (`FLOW_MESSAGING_LIVE_KEY`).
3. Connect your own Telegram bot (`POST /v1/senders`, quickstart section 7); iMessage, SMS and WhatsApp numbers are connected by the Flow team.
4. Register the webhook again with the live key (endpoints belong to one mode). Nothing else in the code changes.

### Multi-tenant SaaS: one bot per customer

One app holds many senders: connect each customer's bot with your live key (`POST /v1/senders`) and keep the mapping customer to `snd_...` in your database. Events name the sender (`conversation.sender`) and the conversation, so route by those; replies leave from the conversation's own bot. Apps cannot be created through the API or the dashboard today (each sign-in has one app; up to 10 sandbox apps can be claimed), so per-customer apps are not self-serve. A key belongs to one app and one mode.

### Delivery guarantees

* Inbound: every message becomes an event, stored before Flow answers the channel, delivered at least once and in order per conversation (dedupe on `event.id`). Webhooks retry for 3 days (5 s up to 12 h apart); the event log keeps everything for 30 days.
* Outbound: a `202` means stored and queued; every send then ends `message.sent` or `message.failed`, never silently. One conversation sends one message at a time, in order. Retryable channel errors are retried by Flow. When a send's outcome is unknown (the channel took it but never answered), Flow resends it once, so a rare duplicate is possible on Telegram; if that is unknown too, the message fails with `channel_timeout_unknown_outcome` (it may have arrived: check before sending again).
* Requests: `Idempotency-Key` on every POST makes your own retries safe for 24 hours; it cannot undo the channel-side resend above.

### Sent but not received: a checklist

1. **Did the send end?** `GET /v1/messages/{message_id}`, or `GET /v1/events?conversation=conv_...&type=message.sent&type=message.failed`. `queued`: still waiting (an earlier message in the conversation, the sender's pace, or a bot whose token Telegram rejected: reconnect it). `sent`: the channel took it (Telegram reports nothing after that). `failed`: read `error.type`, `error.channel_code` and `error.hint`.
2. **Common `message.failed` codes**: `permission` with `sandbox_contact_left` (the person joined another app on the shared sandbox bot while the message waited: they send your join code again); `channel_error` with `channel_code` `403` on Telegram (the person blocked the bot, or never started it); `outside_window` (iMessage: they have not written recently; WhatsApp: `window_closed`, send a template; `queued_too_long`); `channel_timeout_unknown_outcome` (may have arrived).
3. **Sandbox (test key)**: the person joined *your* app: opened the sandbox bot's `address.link` and tapped Start, or sent your join code. One person belongs to one app per sandbox bot, so joining another app moves them (sends answer `403 permission`). A key made without an account expires after 7 days and its contact is removed. Allowance refusals are `403 permission` (`sandbox_allowance_used`, `sandbox_contact_limit`).
4. **Your own bot (live key)**: the person pressed Start on that bot (a Telegram bot cannot write first), and you send with the live key: sandbox conversations belong to the test key and answer `404 not_found` with a live one.
5. **On the phone**: the chat with the right bot (the sandbox bot and your own bot are different chats) on the Telegram account that joined.
6. **Still stuck?** Write to the Flow team at [samyak@flow.engineer](mailto:samyak@flow.engineer) or [prabal@flow.engineer](mailto:prabal@flow.engineer) with the message ID (`msg_...`), conversation ID (`conv_...`), the call's `request_id`, test or live, and the time. Never your API key.

### Tests in CI, without a phone

Use a test key and simulated conversations: `POST /v1/test/conversations`, post the contact's turns to `.../inbound`, assert on your agent's replies with `.../transcript`. Free, no phone, parallel-safe; at most 500 simulated conversations per app (reuse them) and 120 requests a minute to `/v1/test/`. Keep the key in your CI's secret store. In tests, wait for events (stream or `GET /v1/events`), never fixed sleeps.

### Give a cofounder access

Not built yet: an account has one member (whoever signed in), and there are no invitations. Today a cofounder can sign in to get their own account, or you can hand them an API key from your Keys page (it acts as the app, not as them; revoke it when no longer needed).

## What the Flow team arranges, and how to ask

Some things are set up by the Flow team, not through the API: SMS numbers, WhatsApp numbers (Flow's or your own WhatsApp Business Account), iMessage lines, pricing and support. Write to the Flow team at [samyak@flow.engineer](mailto:samyak@flow.engineer) or [prabal@flow.engineer](mailto:prabal@flow.engineer). For each, what to do today, what to build meanwhile, and what to send.

**Never send secrets in chat or email, and never paste them to a coding agent or commit them**: API keys, bot tokens, provider auth tokens, access tokens, app secrets. The Flow team tells you how to hand credentials over, and Flow stores them encrypted. A coding agent that finds credentials in `.env` or a secret manager leaves them where they are and tells the person this.

**Build meanwhile on Telegram or simulated conversations.** Your code does not change per channel: you reply into conversations, switch on `content.type` and `error.type`, and set `"fallback": "auto"`. Simulated conversations (`POST /v1/test/conversations` with `"channel": "sms"`, `"whatsapp"` or `"imessage"`) keep each channel's rules (SMS consent and STOP, WhatsApp's 24-hour window and templates, iMessage replies only), are free and need no number. When the number or line is connected, switch to a live key; nothing else changes.

| You want | Today | Send the Flow team |
| - | - | - |
| **US SMS** (in pilot), including on your own SMS provider account | `POST /v1/senders` with `"channel": "sms"` answers `501`; the team connects the number. Build on simulated SMS. | Your app ID (`app_...`, from `GET /v1/app` with your live key) and account ID (`account` there); the number in E.164 (`+1` and ten digits), 10DLC or toll-free, and its registration status; whether it is on your own provider account (which provider) or Flow should provide one; what you send and how people opt in. Not the auth token: the team arranges that. |
| **WhatsApp on your own WhatsApp Business Account** (credentials in your own secret manager is fine) | `POST /v1/senders` with `"channel": "whatsapp"` answers `501`; the team connects the number. Build on simulated WhatsApp (templates are approved at once there). | Your app and account IDs; the phone number ID and WhatsApp Business Account ID (in WhatsApp Manager); whether the number is on your own Meta app. Not the access token or the app secret: the team arranges how they reach Flow. Meta bills you directly, so the account needs a payment method in WhatsApp Manager. |
| **iMessage** | Lines are connected by the team; they reply only (the person writes first) unless the line is enabled to start conversations. Build on simulated iMessage or Telegram. | Your app and account IDs; what the agent does; expected people per day; whether you need to write first (and why the person expects it). |
| **Python** | No Python SDK yet (TypeScript first). Use plain HTTP and the WebSocket: the quickstart's snippets come in Python too, and the plain-HTTP examples are tested in Python. | Nothing needed; ask if you want to hear when the SDK ships. |
| **Pricing and billing** | Billing is not live, so everything is free today. Payments are coming; there are no prices or dates yet. With your own provider account (your own SMS provider account, or your own WhatsApp Business Account), the provider bills you directly and Flow charges nothing while billing is not live. | Expected volume per channel and month, if you want to talk about it. |
| **Support: "sent but not delivered"** | First read the message: `GET /v1/messages/{message_id}` and its events (`GET /v1/events?conversation=conv_...`). Every send ends `message.sent` or `message.failed` (with `error.type`, `channel_code` and `hint`); Telegram never reports `message.delivered`, so `sent` is the last status there. | The message ID (`msg_...`), conversation ID (`conv_...`), the `request_id` of the call (also the `Flow-Request-Id` header), test or live, the time, and what you expected. Not your API key, and no message content unless asked. |


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