Skip to main content
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.
  • 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 POSTs 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 202s.

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, ?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 or 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 or 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.