Paste this into Claude Code, Codex or Cursor to have it do the integration.
1. Get a key and set up
First check for a key: ifFLOW_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.
.env, init gets one (POST /v1/sandbox/keys) without asking, then:
- writes
FLOW_MESSAGING_KEYandFLOW_CLAIM_TOKENto.envand adds.envto.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
--yesyourself), installs the Claude Code skill in.claude/skills/flow-messaging/, adds a Flow Messaging section toAGENTS.md(read by Codex and other agents), and registers the MCP serverflowfor Claude Code (.mcp.json) and Codex (codex mcp add).
--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:
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:
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 athttps://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/config.toml):
~/.cursor/mcp.json):
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:
initinstalls 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
- Receive
message.receivedevents. In a web app, add a webhook route withflow.webhooks.handler({ onEvent })(orconstructEventon the raw body). In a worker or script, usefor await (const event of flow.events.stream({ types: ["message.received"] })). - Read what the person said with
contentText(event.data.message.content); it covers text, captions, voice note transcripts and button taps. - Answer with
event.conversation.reply(x), wherexis 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 }. - Register the webhook:
POST /v1/webhook_endpointswith the public URL and["message.received"]; save the returned secret asFLOW_MESSAGING_WEBHOOK_SECRET. Locally, with no public URL, read events from the live stream instead:GET /v1/stream(WebSocket, resumable withafter;flow.events.stream(...)in TypeScript). See Local development. - Handle errors by
error.type(TypeScript:err instanceof OutsideWindowErroretc.). Each error carries ahintand adoc_urlsuch as https://api.flow.engineer/docs/errors/outside_window.
TypeScript (minimal complete integration)
6. Verify end to end
Do not finish until this works:- Ask the user to join the sandbox from their phone, if they have not: give them the sandbox sender’s link from
sandbox_join(orGET /v1/senders). On Telegram, opening the link and tapping Start joins; otherwise they sendjoin <code>, for examplejoin wild-otter-04508705. - Start the app (reading
GET /v1/streamwhen developing locally without a public URL). - If the Flow MCP tools are available to you, call
send_test_messageor ask the user to send a message, thenwait_for_eventformessage.received. Without them, ask the user to send a message and read it withGET /v1/events?type=message.received. After a send, wait formessage.sentormessage.failed(notmessage.delivered, which not every channel reports). Eachwait_for_eventcall waits up to 50 seconds (a longertimeout_secondsis clamped to 50, with a note, not refused); if it times out, call it again withafterset to thenext_afterit returned. - If a delivery failed, read it with
get_webhook_deliveries(MCP), fix the handler, andreplay_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, orPOST /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 checkGET /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 approvedtemplateinstead.) - Test keys only while building. Do not switch to
fk_live_unless the user asks. - Do not work around the sandbox allowance. On
403 permissionwithchannel_codesandbox_allowance_usedorsandbox_contact_limit, or401 authenticationwithsandbox_key_expired, do not retry and do not get more keys: ask the user to sign in (npx @flow-engineer/messaging login --no-wait, thenloginonce they approve).