Skip to content

Ask the docs

Answers come only from these docs and are written by an AI model (Gemini), so check the linked pages. Your question is not kept. Do not include personal details or keys.

Test your Messaging API integration from a coding agent.

Flow Messaging has a hosted Model Context Protocol server at https://api.flow.engineer/mcp. With it in your coding tools, your coding agent can integrate the API and verify it end to end: get a sandbox join link, send a real test message, wait for the reply, and see exactly what your webhook answered.

  • Transport: Streamable HTTP (MCP protocol 2025-06-18 or later). The server keeps no sessions, so every request stands alone.
  • Auth: your Flow Messaging API key, Authorization: Bearer fk_test_... or fk_live_.... The key’s mode decides which tools the agent sees.

MCP is not a runtime dependency: the app you build talks to Flow through the REST API (or the TypeScript or Python SDK, which wrap it) for everything it does at runtime. The hosted MCP server at https://api.flow.engineer/mcp is an optional tool for testing and operating the integration 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. If its tools are already available to a coding agent, it can use them for sandbox testing.

No key yet? Get a test key with no account in one call (the MCP server needs a key, so do this first; without one, /mcp answers 401 with this call in its hint):

curl -X POST https://api.flow.engineer/v0/sandbox/keys

Save key and claim_token (both shown once). Keep the key in an environment variable, never in a file you commit:

export FLOW_MESSAGING_KEY=fk_test_...
export FLOW_CLAIM_TOKEN=fct_...

This key allows 1 contact and 50 messages on the Telegram sandbox and expires after 7 days; a person signs in with a Flow account (npx @flow-engineer/messaging login) to keep the app and send 100 messages to each of 3 contacts. See Keys and sign-in. Signed in, you make live keys (fk_live_...) in the dashboard and go live with your own Telegram bot or WhatsApp number; dedicated iMessage lines are arranged with the Flow team.

The project owner adds it; a coding agent shows them the line for their tool:

Claude Code:

claude mcp add --transport http flow https://api.flow.engineer/mcp --header "Authorization: Bearer $FLOW_MESSAGING_KEY"

Codex (~/.codex/config.toml):

[mcp_servers.flow]
url = "https://api.flow.engineer/mcp"
bearer_token_env_var = "FLOW_MESSAGING_KEY"

Cursor (~/.cursor/mcp.json):

{"mcpServers": {"flow": {"url": "https://api.flow.engineer/mcp", "headers": {"Authorization": "Bearer ${env:FLOW_MESSAGING_KEY}"}}}}

Two traps: set FLOW_MESSAGING_KEY in the shell before running the Claude Code command (the shell fills the key in once, when you run it; unset, the header is saved as Bearer with no key and every call answers 401), and for Codex or Cursor have it in the environment they start with. A server added while the client runs loads only after a restart (in Claude Code, a new session). Added with an empty key already? export FLOW_MESSAGING_KEY=fk_test_..., then claude mcp remove flow, run the add command again, and restart.

Point a Streamable HTTP MCP client at https://api.flow.engineer/mcp and send the header Authorization: Bearer <your key> on every request.

With a fk_test_ key the agent gets build-time tools. Nothing they do reaches anyone who has not joined your app’s sandbox.

ToolWhat it does
whoamiThe app, account and mode behind the key, its senders and webhook endpoints, and a cursor for wait_for_event.
sandbox_joinYour join code, the link for each sandbox sender (today the Telegram sandbox bot) that joins your app, and who has joined. Pass include_qr: true to add a QR code (as text, SVG and PNG).
send_test_messageSends a message to someone who joined, through the real send gate: one kind object, as in a webhook reply ({"kind": "buttons", ...}), or a string for a basic message’s text. With one joined person it needs only the message.
wait_for_eventWaits up to 50 seconds (default 25, under the 60-second tool timeout of common MCP clients; a longer timeout_seconds is clamped to 50, with a note) for an event (filter by types and conversation) and returns it the moment it lands; call again with after set to its next_after to wait longer.
list_eventsReads your app’s event log, like GET /v0/events.
get_webhook_deliveriesShows each delivery to your webhook: the request Flow sent, your status code and the start of your answer, the error and the next retry.
replay_eventSends an event to your webhook again, for example after you fixed your handler.
explain_errorThe page for an error type: what it means, why, and how to fix it.

A typical session, in the agent’s words:

  1. whoami, then sandbox_join: “Open this link on your phone and tap Start.” (If the link can’t be used, the person sends the bot the sender’s join message, for example Hi Sandbox app (ref wild-otter-04508705).)
  2. wait_for_event with types: ["conversation.started", "message.received"].
  3. send_test_message with text: "Hello from my agent".
  4. wait_for_event with types: ["message.sent", "message.failed"] and after set to the cursor the send returned. Wait for these two, not message.delivered: every send ends in one of them, while delivery is reported only by some channels. If the call times out (at most 50 seconds; a longer timeout_seconds is clamped to 50, with a note), call it again with the next_after it returned.
  5. After wiring a webhook: get_webhook_deliveries to see what your server answered, fix it, replay_event, and check again.

With a fk_live_ key the agent gets production tools. They reach real people through your dedicated senders, and every send passes the same send gate as the REST API (channel rules, budgets, pacing).

ToolWhat it does
send_messageStarts a conversation from a dedicated sender with a basic or template message (spends its new-contact budget). On Telegram only people who started your bot can be reached; iMessage lines are reply-only unless writing first is switched on for the line (imessage.can_initiate).
replySends a message (a kind object, or a string for basic text) into an existing conversation.
reactSets or removes your reaction on a message.
typingShows the typing indicator. It clears itself when your next message goes or after a few seconds.
edit_messageEdits the text of a message you sent. Refused past the channel’s limit, as in the REST API.
unsend_messageUnsends a message you sent. Refused past the channel’s limit, as in the REST API.
list_conversationsLists conversations, newest first.
get_conversation_messagesReads a conversation’s messages.

whoami and explain_error work with both kinds of key. Sends take an optional idempotency_key: calling again with the same key returns the first message instead of sending twice.

Tools are annotated for your client: reads are readOnlyHint: true; sends are additive (destructiveHint: false); react replaces your earlier reaction, so it is marked destructive and idempotent.

A refused call returns the same typed error as the REST API, as the tool’s error:

{"error": {
"type": "unsupported_content",
"channel": "telegram",
"param": "kind",
"message": "Carousels work on WhatsApp only.",
"hint": "On Telegram, send a basic message, then one buttons message with an image header per card.",
"doc_url": "https://api.flow.engineer/docs/errors/unsupported_content"}}

hint says what to change for this case, and doc_url points at the error’s page (see Errors for each type). Agents usually fix the call from the hint alone; explain_error gives them the full page.

The server takes API keys today. OAuth sign-in, so a client can connect without a key in its configuration, is planned; key-based setups will keep working.