Skip to main content
This page is written for AI coding agents: follow it step by step to give the user’s AI agent Telegram or iMessage with Flow Messaging. What exists today: Telegram is live (the shared sandbox bot with a test key, the user’s own bot with a live key). iMessage is live for replies only, on lines the Flow team connects to the user’s app; it is not in the sandbox. WhatsApp is not available yet (it waits on Meta’s approval), and there is no SMS. If the user asks for WhatsApp, say so and build on Telegram: the code does not change per channel.

Paste this into Claude Code, Codex or Cursor to have it do the integration.

Open in Cursor

1. Get a key and set up

First check for a key: if FLOW_MESSAGING_KEY is set (in the environment or .env), use it. Otherwise get a test key yourself, with no account, in one call. Do not invent a key, and do not get a second one when you already have one.
With no key in the environment or .env, init gets one (POST /v1/sandbox/keys) without asking, then:
  • writes FLOW_MESSAGING_KEY and FLOW_CLAIM_TOKEN to .env and adds .env to .gitignore;
  • prints the sandbox link and join code, the allowance and the expiry;
  • with the user’s approval (they are the project owner, so leave the answer to them and never pass --yes yourself), installs the Claude Code skill in .claude/skills/flow-messaging/, adds a Flow Messaging section to AGENTS.md (read by Codex and other agents), and registers the MCP server flow for Claude Code (.mcp.json) and Codex (codex mcp add).
Flags: --yes (no questions; the owner’s choice, not an agent’s), --key fk_test_... (use a key you already have), --no-agent-files, --no-mcp, --no-codex, --dir <path>. Without the CLI, the same key by HTTP:
Save key as FLOW_MESSAGING_KEY and claim_token as FLOW_CLAIM_TOKEN in .env (which must be in .gitignore); both are shown once. The answer also holds the app’s sandbox_join_code and the sandbox senders with their links; later, GET /v1/app and GET /v1/senders give them again. This key has a sandbox allowance: 1 contact and 50 messages in total on the Telegram sandbox, and it expires after 7 days. Tell the user, and offer to sign them in to keep the app (3 contacts x 100 messages, no expiry): run npx @flow-engineer/messaging login --no-wait, give them the link and code it prints, and once they say they approved, run npx @flow-engineer/messaging login to collect the new key. Full details, the curl version and the errors: Keys and sign-in. Then add the SDK to a TypeScript or JavaScript project:
The Python and Go SDKs are not published yet: in those languages call the HTTP API directly, as each page’s Python and Go examples show.

2. Environment variables

Never hard-code keys, never commit .env, and never put a key in browser or mobile code.

3. The 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 SDK, which wraps 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:
Codex (~/.codex/config.toml):
Cursor (~/.cursor/mcp.json):
Test keys get sandbox_join, send_test_message, wait_for_event, list_events, get_webhook_deliveries, replay_event; live keys get send_message, reply, react, typing, list_conversations, get_conversation_messages; both get whoami, capabilities, explain_error. After a test send, wait for message.sent or message.failed, never message.delivered (Telegram never sends it). npx @flow-engineer/messaging mcp is a stdio bridge to https://api.flow.engineer/mcp that reads FLOW_MESSAGING_KEY from the environment or .env, for clients that run MCP servers as commands. Full tool list: MCP server.

4. The skill

  • Claude Code: init installs the skill in .claude/skills/flow-messaging/. It tells Claude how to integrate, which errors to handle and how to test.
  • Any agent: a skill file is published at https://docs.flow.engineer/skill.md. Install it with npx skills add https://docs.flow.engineer.

5. Integrate: what to write

  1. Receive message.received events. In a web app, add a webhook route with flow.webhooks.handler({ onEvent }) (or constructEvent on the raw body). In a worker or script, use for await (const event of flow.events.stream({ types: ["message.received"] })).
  2. Read what the person said with contentText(event.data.message.content); it covers text, captions, voice note transcripts and button taps.
  3. Answer with event.conversation.reply(x), where x is a string or the user’s LLM stream (OpenAI, Anthropic, Vercel AI SDK, OpenAI Agents SDK, Claude Agent SDK, LangChain, Mastra). Pass { idempotencyKey: event.id }.
  4. Register the webhook: POST /v1/webhook_endpoints with the public URL and ["message.received"]; save the returned secret as FLOW_MESSAGING_WEBHOOK_SECRET. Locally, with no public URL, read events from the live stream instead: GET /v1/stream (WebSocket, resumable with after; flow.events.stream(...) in TypeScript). See Local development.
  5. Handle errors by error.type (TypeScript: err instanceof OutsideWindowError etc.). Each error carries a hint and a doc_url such as https://api.flow.engineer/docs/errors/outside_window.
TypeScript (minimal complete integration)

6. Verify end to end

Do not finish until this works:
  1. Ask the user to join the sandbox from their phone, if they have not: give them the sandbox sender’s link from sandbox_join (or GET /v1/senders). On Telegram, opening the link and tapping Start joins; otherwise they send join <code>, for example join wild-otter-04508705.
  2. Start the app (reading GET /v1/stream when developing locally without a public URL).
  3. If the Flow MCP tools are available to you, call send_test_message or ask the user to send a message, then wait_for_event for message.received. Without them, ask the user to send a message and read it with GET /v1/events?type=message.received. After a send, wait for message.sent or message.failed (not message.delivered, which not every channel reports). Each wait_for_event call waits up to 50 seconds (a longer timeout_seconds is clamped to 50, with a note, not refused); if it times out, call it again with after set to the next_after it returned.
  4. If a delivery failed, read it with get_webhook_deliveries (MCP), fix the handler, and replay_event; without the MCP tools, fix the handler and have the user write again.

Rules

  • Build on the REST API (or the TypeScript SDK); the MCP server is never a runtime dependency.
  • Use the SDK in TypeScript/JavaScript; do not hand-roll HTTP calls there.
  • Reply into the conversation (event.conversation, or POST /v1/conversations/{conversation_id}/messages). Never choose a channel per message and never assume a contact has a phone number.
  • Verify webhook signatures on the raw body before trusting it; deduplicate on the event id.
  • Answer webhooks within 10 seconds. Run slow agents after the response (after(), waitUntil(), a queue) or use the event stream.
  • Do not send unsupported content blindly. Set fallback: "auto" or check GET /v1/capabilities. reply() already sets it.
  • Never message first on iMessage: the person writes first; on outside_window, wait for them to write, do not retry the same content. (WhatsApp, once available: send an approved template instead.)
  • Test keys only while building. Do not switch to fk_live_ unless the user asks.
  • Do not work around the sandbox allowance. On 403 permission with channel_code sandbox_allowance_used or sandbox_contact_limit, or 401 authentication with sandbox_key_expired, do not retry and do not get more keys: ask the user to sign in (npx @flow-engineer/messaging login --no-wait, then login once they approve).

Docs for agents