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:- Open the stream first.
GET /v1/streamwithoutafterstarts at the moment it opens: anything earlier is not replayed. Once it is open, nothing later is missed. - 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). Skipsimulated: trueones. Only when there is none, show the join link and wait forconversation.started. - Confirm the send. The
202only means queued: wait formessage.sentormessage.failedwith that message’sid(Telegram never sendsmessage.delivered). - Wait for the reply with a timeout. The next
message.receivedin 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
reconnectframe. Reconnect withafterset to the last eventidyou 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;afteris the last event you handled; page whilehas_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}/inboundon a simulated conversation (POST /v1/test/conversations): free, and it arrives as the samemessage.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 inbot); an agent cannot. Ask them: “Create the bot in @BotFather and put its token in.env(or your secret store) asTELEGRAM_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-Keyper 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_..."onPOST /v1/messages; replies go into the conversation and leave from its bot. Every event names itsconversation.sender, so route by sender.
Serverless (Vercel, Workers, Lambda): webhooks only
- Register
https://<your-app>/api/flowonce withPOST /v1/webhook_endpointsand store thewhsec_...secret asFLOW_MESSAGING_WEBHOOK_SECRET. VerifyFlow-Signatureon the raw body (in TypeScriptflow.webhooks.handler(...)does it), dedupe onevent.id, answer within 10 seconds. - Fast answer: return
{"reply": ...}in the webhook answer. Slow agent: answer200 {}at once, then reply withPOST /v1/conversations/{conversation_id}/messages(on Vercel insidewaitUntil). Keep conversation state in your database, keyed byconversation.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/flowreads the stream andPOSTs each event to your handler, signed like a real delivery withFLOW_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, ordocs/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 with422 unsupported_contentuntil 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.receivedwith{"type":"voice","file_id":"file_...","url":"...","duration_seconds":4}. Download it withGET /v1/files/{file_id}(your API key; it answers302to 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 approvedtemplate 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/messagesper person, each with its ownIdempotency-Key(for examplecampaign-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_limitedwithRetry-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 answers429 new_contact_limitwithretry_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_throttledon 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.sentormessage.failed: count those, not the202s.
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
- Make a new key: dashboard (https://api.flow.engineer/admin/keys,
?mode=livefor live keys) > Create live key (or test). Put it where the old one was and deploy. - 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). - Read
GET /v1/eventsfor 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).
Prototype to production
- Sign in to keep the app (
npx @flow-engineer/messaging login): the app, its data and webhooks stay. - Dashboard > Live > Create live key; keep it apart from the test key (
FLOW_MESSAGING_LIVE_KEY). - Connect your own Telegram bot (
POST /v1/senders, quickstart section 7); iMessage, SMS and WhatsApp numbers are connected by the Flow team. - 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
202means stored and queued; every send then endsmessage.sentormessage.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 withchannel_timeout_unknown_outcome(it may have arrived: check before sending again). - Requests:
Idempotency-Keyon every POST makes your own retries safe for 24 hours; it cannot undo the channel-side resend above.
Sent but not received: a checklist
- Did the send end?
GET /v1/messages/{message_id}, orGET /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: readerror.type,error.channel_codeanderror.hint. - Common
message.failedcodes:permissionwithsandbox_contact_left(the person joined another app on the shared sandbox bot while the message waited: they send your join code again);channel_errorwithchannel_code403on 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). - Sandbox (test key): the person joined your app: opened the sandbox bot’s
address.linkand tapped Start, or sent your join code. One person belongs to one app per sandbox bot, so joining another app moves them (sends answer403 permission). A key made without an account expires after 7 days and its contact is removed. Allowance refusals are403 permission(sandbox_allowance_used,sandbox_contact_limit). - 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_foundwith a live one. - 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.
- 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’srequest_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.