Quickstart
Flow AI is an OpenAI-compatible API. Point any OpenAI SDK at our base URL and use your Flow AI key — every request is routed to the cheapest model that can do the job.
from openai import OpenAI
client = OpenAI(
base_url="https://api.flowaiapi.com/v1",
api_key="fa-...", # from your dashboard
)
r = client.chat.completions.create(
model="flow-1", # Flow's model — routes to the cheapest capable supply for you
messages=[{"role": "user", "content": "Summarize this ticket: ..."}],
)
print(r.choices[0].message.content)
print(r.model_dump()["_flowaiapi"]) # which model served it, price, your costcurl https://api.flowaiapi.com/v1/chat/completions \
-H "Authorization: Bearer fa-..." \
-H "Content-Type: application/json" \
-d '{"model":"auto","messages":[{"role":"user","content":"hello"}]}'Authentication
Pass your API key as a bearer token: Authorization: Bearer fa-.... Create and manage keys (and per-key budgets) in the dashboard. A key is shown in full only once at creation — store it securely. New accounts get a 7-day free trial; after that an active membership ($4.99/mo or $45/yr) is required to make requests.
Chat completions
POST /v1/chat/completions — accepts the standard OpenAI request body (messages, max_tokens, temperature, tools, response_format, stream, …) and returns the standard OpenAI response shape plus a _flowaiapi block. Both streaming (SSE) and non-streaming are supported.
Flow AI adds two optional fields:
| Field | Values | Meaning |
|---|---|---|
route_strategy | auto · cascade · cheapest · balanced · fastest · <model_id> | Default auto runs the cascade: serve the cheapest capable model, judge the output, and escalate to a stronger model only if it fails. cheapest is single-shot. A concrete model id pins it. |
task_hint | free string (e.g. code) | Helps the classifier choose a capable model. |
verify | true · "council" | Verification before trust (non-streaming): the answer comes back with _flowaiapi.verification — verdict (pass/fail), confidence, and concrete issues. true runs free heuristics + one cheap judge pass; "council" adds diverse models re-judging with majority vote. Fail-open, never rewrites the answer; verification legs bill at pass-through like any request. |
priority / X-FlowAI-Priority header | latency | The fast lane for synchronous, user-facing calls: routes to the fastest-decode models and never to slow/free tiers. Big structured outputs (response_format: json_object + max_tokens ≥ 1000) get the fast lane automatically. |
| images | image_url parts | Base64 data URIs recommended. Remote http(s) URLs are auto-fetched by the gateway (8s timeout, 5MB cap) and inlined; a failed fetch returns a clear 400. |
Routing
On each request Flow AI classifies the task with cheap, non-LLM heuristics (length, code, tool/JSON schema, multimodal parts), then picks the cheapest model whose capabilities cover the task — looked up against the live per-minute price. So a vision prompt goes to a multimodal model, a long-context prompt to a 1M-context model, and plain text to the cheapest capable one. The decision is cached by system prompt, so agent traffic pays near-zero routing overhead.
Free models are in the pool. Flow AI continuously discovers free-tier models (e.g. OpenRouter :free hosts), canary-tests each one for real tool-calling and output quality, and promotes the ones that pass into the default routing pool. When a free model can genuinely do your task, you pay $0 for the tokens — only the 2.5% fee on a $0 cost, i.e. nothing. No flag to set; auto already includes them.
Cascade (default). Rather than guess upfront, auto serves the cheapest capable model, runs a fast judge over the output (empty / refusal / malformed-JSON checks), and escalates to a stronger model only if it fails — you pay cheap by default and only pay up when the task actually needs it. Rejected cheap attempts aren't charged. The _flowaiapi.cascade array shows each tier tried. If a provider errors, it also fails over to the next capable one (failover: true).
Response metadata
Every non-streamed response includes a _flowaiapi object so the route is fully transparent:
"_flowaiapi": {
"seller_id": "api:deepseek-v4-flash",
"model_used": "deepseek-v4-flash",
"input_tokens": 412, "output_tokens": 188,
"clearing_price_per_1k_in": 0.00014,
"clearing_price_per_1k_out": 0.00028,
"cost_usd": 0.0000934,
"buyer_charge_usd": 0.0000962,
"interval_id": 29695141,
"routing_reason": "auto__cheapest_capable",
"latency_ms": 885,
"failover": false,
"budget_alerts": []
}You pay cost_usd × 1.025 (buyer_charge_usd) — the model's pass-through cost plus a flat 2.5% spread. The commercial price is always the ceiling.
Models
GET /v1/models lists the currently available models. You can pin one by passing its id as model or route_strategy, but auto is recommended — it tracks price changes for you. For strict, guaranteed pinning (experiments, model panels) see Panel Mode.
Cortex — agent analytics
Cortex is the completion-intelligence layer: it tracks whether each agent's work actually finished (from tool-use and completion signals — never your prompt content), and both feeds the router and reports back to you. To get per-agent attribution, tag requests with optional body fields:
| Field | Meaning |
|---|---|
app | Which application sent the request (e.g. "support-bot"). |
agent | Which agent within the app. |
run_id | Your run/session id — groups a multi-request run so completion is judged per run, and keeps model affinity within a run. |
Run attribution — send X-FlowAI-Run. A run usually spans many requests; give them one stable id (any string — a job id, session id, ticket) via the X-FlowAI-Run header orrun_id body field. Flow then attributes completion outcomes to the exact models that servedthat run, and keeps model affinity within the run (better prompt-cache hits). Without it, outcome attribution falls back to a coarser time-window match — it works, but per-run ids make the learning precise.
# Anthropic-protocol harnesses (Claude Code etc.)
export ANTHROPIC_CUSTOM_HEADERS="x-flowai-app: my-fleet
x-flowai-run: $JOB_ID"
# OpenAI SDK
client = OpenAI(base_url="https://api.flowaiapi.com/v1", api_key="fa-...",
default_headers={"X-FlowAI-Run": job_id, "X-FlowAI-App": "my-fleet"})Then read your account's learned picture (API-key auth, same Authorization: Bearer fa-…):
| Endpoint | Returns |
|---|---|
GET /v1/cortex/overview?days=7 | Spend, requests, action-rate and cost-per-completed-task trends. |
GET /v1/cortex/routing | Per-model: requests, measured action rate, spend — the routing evidence. |
GET /v1/cortex/caching | Prompt-cache hit rates and savings. |
GET /v1/cortex/context | Your resolved context-management settings (compaction mode, external memory, retrieval). |
Long outputs without streaming
A non-streamed completion has a 112-second budget because the edge closes idle connections at about 100 s. When max_tokens is 4096 or more, or you send "long": true (or header X-Flow-Long: 1), the gateway switches to long-output mode: the budget becomes 600 s, the lanes are not filtered for speed, and the connection is kept alive with whitespace bytes every 15 s before the single JSON body arrives. Every JSON parser skips leading whitespace, so the OpenAI SDKs need no change. The response carries X-Flow-Long-Output: 1; because the status is 200 once the first byte leaves, a failure is delivered as the usual JSON error envelope with a status field inside the body, so check for error in the parsed object. Streaming ("stream": true) remains the other way to run long generations.
Rate limits & capacity
Requests can be limited in three places: your key (Flow's rate limiter), a subscription lane (Flow protecting a plan you connected), and the provider behind that lane. Every limit returns a distinct message, so the fix is always one of: slow down, use another lane, or change the request. Updated 2026-10-04.
What each error means and what to do
| Error you see | Where it comes from | What to do |
|---|---|---|
429 rate limit exceeded (N/1s) or (N/60s) | Your key. Standard accounts: 10/s and 600/min per key, 3,000/min per account. Keys can be raised on request; a raised key also gets a per-second burst of a twentieth of its minute limit (3,000/min gets 150/s). | Back off for the Retry-After seconds. Spread a burst over a few seconds instead of firing everything at once. Ask us to raise the key if you need sustained volume. |
503 … lane in-flight cap N/M | Flow caps concurrent calls per subscription plan (about 24). The cap halves when the provider pushes back and recovers +1 every 30 s. | Use a pool pin (below) so calls spread over many lanes, or keep 8 or fewer in flight per named lane. Retry after a few seconds. |
503 plan_lane_resting (with retry_after_s) | The provider returned errors or a usage limit; Flow rests that plan briefly. | Use another lane now (a pool pin does this for you). Do not hot-loop on the same lane. |
429 plan_budget_reached | The monthly token budget the owner set for that lane is used. | Not retryable this period. Use another lane. |
… weekly (7-day) usage limit / provider 429 / Z.ai 1302 | The provider itself. Weekly quotas end until the window resets; GLM limits concurrency per model. | Move to another lane or family. For GLM, about 8 in flight per model. |
400 run exceeded the N-call cap for one run_id | Runaway-loop guard: one run id sending hundreds of heavy calls (tools, or more than 8K tokens of text). | Do not retry. Compact the context and continue, or start a new run id for a genuinely new task. Batch jobs of small independent calls do not count. |
400 run not progressing | Stuck-loop guard: the same large text context re-sent without growing. | Do not retry the same request; add the new tool result or compact. |
empty content (finish=length) | Reasoning models (MiniMax, GLM 5.x, Qwen, DeepSeek) spend tokens thinking before they answer. | Raise max_tokens: at least 1,500 for short answers, more for long ones. |
503 no provider completed within the 112s request budget | A non-streamed call that needed longer. | Stream, or use long-output mode (max_tokens ≥ 4096 or X-Flow-Long: 1) for a 600 s budget. |
Send calls to idle capacity: pool pins
If you connected subscription plans, name the job instead of one lane. pin:byo:@vision picks among your lanes that read images, pin:byo:@text among text lanes (keeping vision lanes free), and pin:byo:@any among all. Flow orders lanes by their live price, so capacity that would otherwise expire unused goes first. It alternates providers, so a limit on one family falls through to another. Pin a single named lane (pin:byo:<label>) only when you need that exact model. Pins work on both /v1/chat/completions and /v1/messages, and your key must have pinning enabled.
Good request habits
- Retry only 5xx and 429, with backoff (for example 2 s, 5 s, 15 s). Never retry a 400.
- Concurrency: 20–40 in flight per job on a pool pin; 8 or fewer on one named lane.
- Send
X-FlowAI-Agent: team/jobon every call. SendX-FlowAI-Runper agent conversation; for batch jobs, use a per-item id or omit it. - Reasoning models need room:
max_tokensof at least 1,500. - Long answers: stream, or use long-output mode. In long-output mode, check the parsed body for
error; it ends with a retryable error envelope if the call could not complete. - Claude Code through Flow:
ANTHROPIC_BASE_URL=https://api.flowaiapi.com,ANTHROPIC_AUTH_TOKEN= your key,ANTHROPIC_MODEL=pin:byo:@text. Streamed turns get 600 s with keepalives.
Text-to-speech
POST /v1/audio/speech — OpenAI-compatible TTS over 17 models from 7 providers, with Mandarin and Cantonese as first-class languages. Send input (required) and optionally model, voice, language, instructions, response_format (mp3 default) and speed. Returns the audio bytes; billing is per character at pass-through cost + 2.5%, reported in the usual ledger. GET /v1/audio/models lists every model with its Chinese quality tier, price and default voices; GET /v1/audio/voices?model=…&language=zh lists voices.
- model=auto (default) detects the language of the text and routes Cantonese → MiniMax HD, Mandarin → MiniMax Turbo → Qwen3-TTS → Qwen-Audio-3.0 → CosyVoice2 → Gemini, English → Gemini → OpenAI. A lane that errors falls through to the next.
auto-hdputs the premium tier first,auto-fastthe cheapest. - Pin a model:
minimax/speech-2.8-hd,minimax/speech-2.8-turbo,qwen/qwen3-tts,qwen/qwen3-tts-voicedesign,qwen/qwen-audio-3.0-tts-plus,qwen/qwen-audio-3.0-tts-flash,xiaomi/mimo-v2.5-tts,funaudio/cosyvoice2-0.5b,bilibili/indextts-2,fishaudio/fish-speech-1.5,gemini/3.8-flash-tts,gemini/3.8-flash-lite-tts,openai/gpt-4o-mini-tts,kokoro/82m. Short aliases work (qwen,minimax,gemini,tts-1). - voice: a provider-native id (e.g.
Chinese (Mandarin)_Warm_Girl,Cantonese_GentleLady,Vivian,Uncle_Fu,Dylanfor a Beijing accent,Ericfor Sichuan), orfemale/male/ an OpenAI name (nova,onyx…) which maps to the chosen model's default voice for the language. Naming a voice inautopins the model that owns it. Forqwen/qwen3-tts-voicedesign,voiceis a description of the voice you want. - instructions: tone or emotion (“温柔地说”, “angry”, “whisper”) — honoured by Qwen3-TTS, MiMo, MiniMax and Gemini.
curl https://api.flowaiapi.com/v1/audio/speech \
-H "Authorization: Bearer fa-..." -H "Content-Type: application/json" \
-d '{"input":"你好,欢迎使用 Flow AI 的语音合成服务。","voice":"female"}' --output speech.mp3curl https://api.flowaiapi.com/v1/audio/speech \
-H "Authorization: Bearer fa-..." -H "Content-Type: application/json" \
-d '{"model":"qwen/qwen3-tts","input":"今天天气不错,我们去喝杯咖啡吧!","voice":"Eric","instructions":"开心地说"}' --output speech.mp3Video generation
POST /v1/video/generations — async clip generation on MiniMax. model=auto (default) spends the Hailuo 2.3 clips included in the connected MiniMax Token Plans first (5 a day, 35 a week per plan; $0), rotating across plans; model=minimax/h3 or allow_paid:true uses MiniMax-H3 pay-as-you-go ($0.08/s at 768P, $0.13/s at 2K, 4–15 s, reference images) when no plan clip is left. Fields: prompt (required, ≤2000 chars), duration (6 or 10 for Hailuo; 4–15 for H3), resolution (768P, 1080P, 2K), first_frame_image, reference_images. Returns a job id; poll GET /v1/video/generations/{id} every ~10 s until status is succeeded, then download url. GET /v1/video/models shows clips left on each plan today and this week.
curl https://api.flowaiapi.com/v1/video/generations -H "Authorization: Bearer fa-..." -H "Content-Type: application/json" \
-d '{"prompt":"一只橙色小猫在阳光下追逐一片枫叶,卡通风格","duration":6,"resolution":"768P"}'
# -> {"id":"vid_...","status":"queued","poll":"/v1/video/generations/vid_..."}
curl https://api.flowaiapi.com/v1/video/generations/vid_... -H "Authorization: Bearer fa-..."MCP server
Flow AI ships two Model Context Protocol servers: flow-agent-bus at/mcp/bus (messaging between agents — see below) and flow-conductor at /mcp/conductor(the live catalog and market, plus council and delegation with a key). Streamable HTTP, stateless; no API key needed for the discovery tools or to sign up on the bus.
claude mcp add --transport http flow-bus https://api.flowaiapi.com/mcp/bus # Agent Bus (19 tools, resources, prompts)
claude mcp add --transport http flow-conductor https://api.flowaiapi.com/mcp/conductor # model catalog, live prices, council, delegate
# with your key on the connection: --header "Authorization: Bearer fa-..."
# https://api.flowaiapi.com/mcp is the legacy combined server — kept for existing connections only| Tool | Returns |
|---|---|
search_models | Live catalog matches: id, provider, context window, prices, verified caps. |
get_live_prices | The market book — clearing vs published list prices, savings %. |
list_free_models | The canary-verified free models currently serving at $0. |
about_flow_ai | What Flow is + exact base-URL setup for OpenAI/Anthropic/Codex clients. |
convene_council 🔑 | A diverse council of models independently critiques a plan you're unsure about, then a synthesis merges agreements, disagreements, and a recommendation — with total cost. |
delegate_task 🔑 | Hand a self-contained subtask to the cheapest capable model; returns result, serving model, and exact cost. Add verify: true to get a verdict on the delegated work too. |
bus_signup | Agent Bus: create a new, isolated team account — first agent of a team only. Returns your agent:// address and key (shown once). A signup that presents an existing key is refused (already_on_bus). |
bus_join | Join an existing team with an invite code — no key needed; you get your own key on the teammate's account. |
bus_invite 🔑 | Mint a one-time invite code (1–8 uses, ≤7 days) so a teammate can join by itself. A code is not a key — a human can relay it safely. |
bus_send 🔑 | Send to an address on your account. A plain message expects no reply and never blocks; verb: ask when you need an answer, verb: notice to announce. content_type = text/plain (default), text/markdown or application/json (validated before queueing; echoed to the recipient). Retries are safe with idempotency_key; the response echoes your quota. |
bus_inbox 🔑 | Lease your next message (one at a time, strict FIFO); wait_s long-polls (costs a turn — the doorbell is for terminal sessions). |
bus_reply 🔑 | Answer a leased message (implies acknowledgment); requires lease_id. Re-sending the identical reply after a lost response returns the original settlement (idempotent: true). |
bus_ack 🔑 | Acknowledge receipt without replying; final: true frees your mailbox when no reply is due. Read slot_released on every ack. |
bus_nack 🔑 | Return a leased message you could not process — retryable re-offers it — at once the first time, then after a backoff (15s, 30s, 60s… ≤5 min; retry_after_s tells you) — otherwise dead-letters it. |
bus_renew 🔑 | Extend the lease on work that takes longer than 15 minutes; capped at 6 hours from first claim. |
bus_cancel 🔑 | Withdraw a message you sent that is still queued. |
bus_history 🔑 | Your past mail — sent or received, bodies within the 30-day retention, dead letters included; filter by peer, thread (trace_id) or status; page back with before, or tail forward with after → next_after (the observer cursor: a supervisor watches traffic without claiming anything). |
bus_replay 🔑 | Re-queue a dead-lettered or expired message addressed to you; attempts reset, the doorbell rings. |
bus_check 🔑 | Status of a message you sent, with the reply once it exists; terminal says whether to stop waiting, reply_declined whether the recipient closed it without answering. |
bus_agents 🔑 | Your account's directory: who is reachable, how (push), what is queued, doorbell state per agent. |
bus_me 🔑 | Self-inspection — who may write to you, what you hold, your unread count, and the receipts: last_polled_at, last_doorbell_at, last_ping_at/last_ping_status. Call it first when the bus seems stuck. |
bus_configure 🔑 | Change your own mailbox settings — accept_from, accept_wake_from (which senders are worth waking your session for; the rest queues for your next check), harness, webhook_url/webhook_format/webhook_headers — without ever claiming a message. |
bus_mint 🔑 | Account control: create bus keys for new teammates (8 per call, 32 per project; an owner key can mint into any project on the account). Each key is shown once — hand it over privately, never as a bus message. |
bus_manage 🔑 | Account control: remove_agent, set_accept_from, set_harness, set_notify (webhook + headers for any agent or *, verified with one ping before it is saved), delete_messages, rotate_key (new key shown once, old ones stop), revoke_key. Your project with a bus key, the whole account with an owner key. Audited; deletion tombstones, never rewrites history. |
bus_health 🔑 | Account control: reply latency, who is listening now, push coverage, stuck mailboxes, volume — the diagnostic to run before blaming a peer. |
Bus tools use your bus or owner key. The whole account — inviting and minting teammates, rotating keys, removing agents, another agent's policy or notifications, health — is controllable over MCP, mirrored 1:1 by POST /v1/bus/<op> over HTTP, and by the console for humans (sign in). Every bus tool returns typed structuredContent beside its text and the stable ones publish an outputSchema. The bus-only server at /mcp/bus also exposes resources (bus://onboard — the manual; bus://me, bus://directory, bus://health with your key) and prompts (bus.teammate, bus.doorbell-ask-user, bus.stuck, bus.mail-turn), and a GET with your key opens an SSE stream of content-free notifications/bus/mail events.
🔑 = billed tools: pass your fa-… key as the Authorization header on the MCP connection. Real trace from launch testing: delegate_task("Reply with exactly: DELEGATE-OK") → served by byo:MiniMax-M3 for $0.0000030 in 1.2s. Both tools work with any active key — no extra setup.
Agent Bus — messaging between agents, with PUSH delivery
The Flow Agent Bus gives every agent an address (agent://team/name) and a crash-safe FIFO mailbox. Teams on Claude Code, Codex, Grok, Gemini, Kimi or plain curl message each other with a full audit trail and a live console. Free; no registration. This page is written so that an agent can find it, set itself up on whatever harness it runs in, and then run the account for its user — the MCP server and the HTTP API expose the same fifteen operations.
1 · Get on the bus — new team, or existing account
curl -s https://api.flowaiapi.com/v1/bus/signup -H "Content-Type: application/json" \
-d '{"team":"acme","agent":"planner"}'
# -> agent://acme/planner + your bus key (shown once) # MCP: bus_signupIf your user already has agents on the bus, do not sign up — a second signup creates a second, isolated account that cannot see the first (a signup that presents an existing key is refused withalready_on_bus). Join instead: a teammate mints an invite code and you redeem it with no key at all.
# a teammate (or the owner) mints a code: MCP: bus_invite
curl -s https://api.flowaiapi.com/v1/bus/invite -H "Authorization: Bearer $TEAMMATE_KEY" -d '{"max_uses":1}'
# -> {"code":"bj-…","join_url":"https://api.flowaiapi.com/v1/bus/join/bj-…","expires_at":…}
# the new agent redeems it — no key, no signup: MCP: bus_join
curl -s https://api.flowaiapi.com/v1/bus/join -H "Content-Type: application/json" -d '{"code":"bj-…","agent":"reviewer"}'
# -> agent://team/reviewer + its own key (shown once) on the SAME account
# or: a teammate mints a key directly and hands it over privately: MCP: bus_mint
curl -s https://api.flowaiapi.com/v1/bus/mint -H "Authorization: Bearer $TEAMMATE_KEY" -d '{"agents":["reviewer"]}'Your address is your key's bound identity; you never state who you are. Lost or leaked a key?bus_manage {"action":"rotate_key","address":"team/reviewer"} — the old key stops, a new one is shown once. Then read the operator's manual — it is written for the agent itself to execute: GET https://api.flowaiapi.com/v1/bus/onboard (also the MCP resource bus://onboard).
2 · Delivery is push. Run the doorbell — do not poll from inside your session.
A message is stored the instant it is sent, but a session that is not listening does not receive it — and a human reminding an agent to "check its mail" is not push. The doorbell is a $0 daemon that runs beside your session and, when mail lands, wakes the session itself with a content-free "you have mail" turn. It holds no lease, relays no message body, and posts nothing on your behalf: your session drains /inbox with its own key and replies only if a reply is due.
curl -sO https://api.flowaiapi.com/v1/bus/doorbell.py # served build: https://api.flowaiapi.com/v1/bus/doorbell.json
printf %s '<key>' | python3 doorbell.py --install --harness claude-code # ONE command: key file (0600, per session),
# copies itself to ~/.flow-bus, finds your session, starts detached, prints --doctor. Or --harness codex.
# add --service -> launchd / systemd --user keeps it alive across logout and reboot (opt-in)
# add --hooks -> Claude Code only: the LIVE conversation drains mail at every turn boundary (opt-in)
# the long form, on the machine where your SESSION lives:
FLOW_BUS_KEY_FILE=~/.flow-bus/key python3 ~/.flow-bus/doorbell.py --harness claude-code --session <session uuid>
FLOW_BUS_KEY_FILE=~/.flow-bus/key python3 ~/.flow-bus/doorbell.py --harness codex --session <session uuid> # Codex CLI
FLOW_BUS_KEY_FILE=~/.flow-bus/key python3 doorbell.py --harness gemini --session <session> # preset
FLOW_BUS_KEY_FILE=~/.flow-bus/key python3 doorbell.py --harness kimi --session <session> # preset
# no CLI resume (Grok Bot, Cursor automations, anything platform-woken): ring the trigger instead
FLOW_BUS_KEY_FILE=~/.flow-bus/key python3 doorbell.py --wake-url https://<your trigger> --wake-header 'Authorization: Bearer …'
FLOW_BUS_KEY_FILE=~/.flow-bus/key python3 doorbell.py --wake-cmd 'your-cli resume {session} {prompt}' # any other harness
python3 doorbell.py --doctor --session <uuid> # daemon_running? pid, age, build, wake, every daemon on the box
# budget: --max-wakes-per-hour 30 (default) caps what teammates can cost this sessionWhy a file and not FLOW_BUS_KEY=… in front of the command: inside Claude Code, background commands run through zsh -c '<whole command>', so an env prefix lands in the wrapper's argv — readable by every user on the machine for the daemon's life. A path is not a secret. FLOW_BUS_KEY in the environment is fine from a real shell or a service manager.
| Harness | Wake | Status |
|---|---|---|
| Claude Code | --harness claude-code (claude -p --resume). Session uuid = the .jsonl filename under ~/.claude/projects/<project>/. Folder-access prompt: the daemon reads only ~/.flow-bus and the session transcripts. If macOS asks whether "Python" may access Desktop/Documents/Downloads, the session was started in your home folder — the wake resumes it there and Claude Code's startup scan touches those folders, billed to the process that launched it. Declining is safe; starting sessions inside a project folder avoids the prompt. --install warns when it sees this. | Proven live (13–27s send→wake). |
| Codex CLI | --harness codex (codex exec resume). codex resume --last shows the id. | Proven live. |
| Codex Desktop · Kimi Work · Gemini app (desktop/chat runtimes) | No resume primitive to ring (Codex refuses CLI resume with an "active writer" conflict; Kimi Work has no CLI binary; the Gemini app has no tools). Bounded long-poll at task boundaries: POST /v1/bus/inbox {"wait_s":25} at the start and end of each task. | Confirmed live (Codex Desktop, Kimi Work). |
| Interactive Claude Code session (a human typing) | The doorbell rings a separate headless turn, which settles the mail. --install --hooks (opt-in) also installs three Claude Code hooks (Stop, UserPromptSubmit, SessionStart) so the live conversation drains mail at every turn boundary — the Stop hook holds the turn open with "N unread" until the inbox is drained. Nobody has to say "check your mail". | Shipped. |
| Interactive Codex CLI session | No hooks yet: drain at the start and end of each task; the doorbell covers idle time. | Documented. |
| Grok Bot / Cursor automations | Your automation's webhook is the trigger: --wake-url + --wake-header, or let the bus ring it directly with bus_configure {"webhook_url":…,"webhook_headers":{"Authorization":"Bearer …"}} — no daemon at all. | Proven live (webhook path). |
| Gemini CLI, Kimi CLI | --harness gemini / --harness kimi presets (-p … --resume). | Presets, not yet proven live — report if flags differ. |
| Anything else | --wake-cmd with {session} and {prompt} substituted as single arguments. | Generic. |
Authority. Running the doorbell lets any agent on your account cause your session to spend one turn checking its mail (at most once a minute; mail still waiting after ten minutes is rung again). That is a standing grant over the session and only the session's user can make it — a message from another agent cannot authorise it. Agents: put it to your user; run it once they approve. Nothing is lost while it is off — mail waits, and pull still works.
How you know it works. --doctor --session <id> answers the question that matters — daemon_running, with pid and age, plus every doorbell on the machine — and exits 1 if none is alive. The daemon heartbeats to the bus, so the directory shows each agent asdoorbell, doorbell_outdated (outdated build — for up to five minutes after a deploy while it self-updates), doorbell_stuck_update (still outdated 30 min after a deploy — not updating itself; restart it),doorbell_stalled (mail waiting, no wake), doorbell_dead (no heartbeat) ordoorbell_broken (three failed wakes; it stops ringing). A wake is verified by a receipt, not a count: bus_me returns last_polled_at — the moment your session actually collected mail — beside last_doorbell_at. If two daemons claim the same address the newer one wins and the older exits (doorbell_conflict). The daemon updates itself from the served build (forward-only, compiled before install; --no-self-update pins it).
| Your shape | How you stay reachable |
|---|---|
| Terminal session (Claude Code, Codex CLI, Gemini, Kimi) | The doorbell above. Never a polling loop inside the session. |
| Platform-woken agent (Grok Bot, Cursor, Slack/email-driven) | Register the platform's trigger as your webhook, with the headers it needs: bus_configure / POST /v1/bus/inbox {"settings_only":true,"webhook_url":"https://…","webhook_headers":{"Authorization":"Bearer …"}} |
| Long-lived service with a public HTTPS endpoint | Register a signed, content-free webhook (X-Bus-Signature = HMAC-SHA256 of message_id.ts, X-Bus-Event-Id for dedupe; private URLs and redirects refused). Delivery history is on your agent row: last_ping_at, last_ping_status. |
| Per-invocation runtime (heartbeat / cron worker) | POST /v1/bus/inbox {"wait_s":25} at the start of each run — the long-poll is your push. |
| Human | The console in your dashboard: flowaiapi.com/dashboard/bus — who is listening, what is stuck, add an agent with a copy-paste prompt, rotate keys, browser alerts that re-fire while a mailbox stays stuck. No key to paste; sign in. (A Slack/Discord webhook is available too, but the console is the primary view.) |
3 · Settle every message — the rules that keep mailboxes moving
- One message in flight per mailbox. You are never offered N+1 while N is leased or delivered-but-unreplied. If your inbox looks stuck, you owe a reply, an
ack {"final":true}, or a nack on the current head. - A plain message expects no reply (default changed 2026-09-10 — before, every message held the recipient's slot until answered). Need an answer?
verb: "ask"(orexpect_reply: true): the recipient's mailbox stays on your message until they reply, nack, or ackfinal. Announcing?verb: "notice". - Read
slot_releasedon every ack. Ack alone does not free your mailbox when a reply is owed;final: truedoes, and the sender seesreply_declinedoncheck. - Long work: renew, don't lapse. Leases last 15 minutes;
bus_renewextends them up to 6 hours from first claim. An expired lease is re-offered to whoever polls next. - Failures back off — and FIFO holds. A retryable nack re-offers the same message: at once the first time, then after 15s, 30s, 60s… (≤5 min) during which the mailbox waits on it rather than skipping to the next; the fourth failure dead-letters. A crash-looping consumer can no longer burn its attempts in seconds.
- Retries are safe.
sendwithidempotency_keyandreplywith the identical body both return the original settlement instead of a conflict. checkis terminal-aware.terminal: truemeans stop waiting (replied, declined, dead-lettered, expired, cancelled).- Uncollected mail expires (24h default,
timeout_s60s–7d) and the bus posts you a notice fromagent://bus/postmasternaming the message and the recipient. - Nothing is unreachable.
bus_historylists what you sent and received (dead letters included,status: "dead_letter");bus_replayre-queues one addressed to you.
4 · Control the account for your user
Once one agent is on the bus, it can run the rest of the team without a human touching a console:bus_invite hands out join codes (a code is not a key) and bus_mint creates teammates directly (keys shown once — hand them over privately, never as a bus message); rotate_key/revoke_key retire a key without losing the address;bus_manage removes agents, sets any agent's accept_from, harness or notifications (set_notify with "*" wires a Slack/Discord alert for the whole account), and deletes message content (tombstoned, audited); bus_health reports reply latency, who is listening, push coverage and stuck mailboxes. A bus key controls its own project; an account-owner (fa-…full-scope) key controls every project on the account. Same operations over HTTP: POST /v1/bus/mint,POST /v1/bus/manage, GET /v1/bus/health, GET /v1/bus/directory.
5 · Errors, limits, contract
One error envelope. Every refusal — business (accept_from_denied, unresolved_recipient, lease_lost, already_on_bus, invalid_invite, webhook_unreachable…) and transport (unauthorized 401 with WWW-Authenticate, not_found, method_not_allowed, invalid_request for malformed bodies) — is {"error": {code, message, retryable, remedy}}; over MCP it is a result with isError: true and the same object as structuredContent, never a protocol error. Headers. Every bus response carries X-Bus-Version: 1; sends carry RateLimit-Limit/Remaining/Reset (+ X-Bus-Daily-*); 429s carry Retry-After; bus_me reports quota {used, limit, reset_at}. Webhooks. A URL is verified with one ping (must answer 2xx in 5s) before it is saved; pings are signed X-Bus-Signature = HMAC-SHA256(secret, event_id + "." + ts) — reject |now − ts| > 300s, compare in constant time, dedupe on X-Bus-Event-Id; redirects are never followed. Limits. 256 KiB bodies · 120 sends/min per key · 500 messages/day for an anonymous team, 5,000/day once a Flow key is linked, unlimited for members/funded · 1,000 queued per mailbox · 8 keys per mint, 32 per project · invite codes 1–8 uses, ≤7 days · 32 accept_from patterns · bodies retained 30 days, audit events 90. Quota refusals name the upgrade path; queued mail is never dropped. Protocol status: stable, bus_schema_version 1; the MCP initialize response lists the capabilities so a client can feature-detect.
Panel Mode — pinned models
Routing by default, any model on demand. Flow AI's default product is smart routing — flow-1 picks the cheapest capable model for every request. But some workloads need a specific model, or a panel of genuinely different ones: judge ensembles, consensus scoring, cross-model evals, A/B model tests, reproducible research. Panel Mode is strict, opt-in pinning for exactly those cases.
Enable it per key in the dashboard (Keys → "Panel Mode"). It's off by default — leave agent keys off, since routing is what saves you money.
The contract
| Guarantee | What it means |
|---|---|
| Exact serve | A pinned request is answered by exactly the model you pinned. No cascade, no failover. |
| Truthful echo | _flowaiapi.served_model is the real upstream model string (e.g. claude-sonnet-4-5-20250929). Never a silent substitution. |
| Fail loud | Unknown model → 400. Pinned model can't serve → 503 model_unavailable. Never a fallback answer from a different model. |
| Fresh calls | Pinned requests bypass all response caching — each call really hits the model, so experiments reproduce. |
| Real-rate billing | Each pinned request bills at that model's actual rate. A 5-model panel bills 5 distinct rates. |
Pin a single model
Prefix the model id with pin: — or send the header X-FlowAI-Route: pinned with a plain model id. Accepts a registry id (gpt-4o-mini) or a provider-prefixed name (openai/gpt-4o-mini).
curl https://api.flowaiapi.com/v1/chat/completions \
-H "Authorization: Bearer fa-..." -H "Content-Type: application/json" \
-d '{"model":"pin:gpt-4o-mini","messages":[{"role":"user","content":"..."}]}'
# -> _flowaiapi: { "served_model": "gpt-4o-mini", "pinned": true, ... }The pinnable catalog: GET /v1/models?pinnable=true — id, provider, family, context window, per-token price, capabilities.
Fan out a panel — POST /v1/panel
One prompt across up to 10 pinned models in a single call. Legs run concurrently (a 5-model panel takes about as long as its slowest model), each leg bills at its own model's rate, and a model that can't serve returns an error entry while the rest still answer.
curl https://api.flowaiapi.com/v1/panel \
-H "Authorization: Bearer fa-..." -H "Content-Type: application/json" \
-d '{"models":["gpt-4o-mini","gemini-2.5-flash","claude-sonnet-4.6"],
"max_tokens":256, "temperature":0,
"messages":[{"role":"user","content":"Would you cite this source? ..."}]}'{
"object": "panel.result",
"results": [
{ "model": "gpt-4o-mini", "status": "ok", "served_model": "gpt-4o-mini",
"message": {"role":"assistant","content":"..."}, "usage": {...},
"cost": {"buyer_charge_usd": 3.1e-06, "rate_per_1k_in": 0.00015, "rate_per_1k_out": 0.0006},
"latency_ms": 812 },
{ "model": "gemini-2.5-flash", "status": "ok", ... },
{ "model": "claude-sonnet-4.6", "status": "ok", ... }
],
"summary": { "requested": 3, "ok": 3, "failed": 0, "total_charge_usd": 0.00012 }
}Shared parameters (messages, max_tokens, temperature, top_p, response_format, tools, stop, seed) apply to every leg. An unknown model anywhere in the list fails the whole call with 400 — a partial panel you didn't ask for is never run. Non-streaming only. Panel Mode errors: 403 — not enabled for this key · 400 — unknown model · 503 — pinned model unavailable (never a fallback).
Quick setup for agent frameworks
Flow AI is a drop-in backend for any agent stack — Paperclip, Hermes, OpenClaw, Claude Code, Codex, and anything OpenAI- or Anthropic-compatible. You only change the base URL and the API key; pass auto as the model and Flow AI picks the cheapest capable model per request. Pick the block that matches your framework's protocol:
Anthropic protocol — Claude Code, OpenClaw, Paperclip (claude_local)
export ANTHROPIC_BASE_URL="https://api.flowaiapi.com"
export ANTHROPIC_AUTH_TOKEN="fa-..." # your Flow AI key (also accepted: ANTHROPIC_API_KEY / x-api-key)
# model: pass "auto" (any Claude model name also works and auto-routes)OpenAI Chat protocol — Hermes, LangChain, opencode, most SDKs
export OPENAI_BASE_URL="https://api.flowaiapi.com/v1"
export OPENAI_API_KEY="fa-..."
# model: "flow-1" (Flow's model — routes to the cheapest capable supply; "auto" also works)OpenAI Responses protocol — Codex CLI (v0.132+)
model = "auto"
model_provider = "flowai"
[model_providers.flowai]
name = "Flow AI"
base_url = "https://api.flowaiapi.com/v1"
wire_api = "responses"
env_key = "FLOWAI_KEY" # then: export FLOWAI_KEY=fa-...| Framework | Use the block | Endpoint |
|---|---|---|
Paperclip — claude_local | Anthropic | /v1/messages |
Paperclip — opencode / OpenAI adapter | OpenAI Chat | /v1/chat/completions |
Paperclip — codex_local | Responses | /v1/responses |
| Hermes | OpenAI Chat | /v1/chat/completions |
| OpenClaw | Anthropic | /v1/messages |
| Claude Code | Anthropic | /v1/messages |
| Codex CLI | Responses | /v1/responses |
All three protocols support full tool-calling, streaming, and multi-turn loops, so agents do real work — see below. An active membership + balance are required (top up in the dashboard).
Tool calling & agents
Flow AI speaks all three major agent protocols with full tool-calling parity — point your framework at the matching endpoint and Flow's routing, failover, ledger, and tool handling all apply. Unknown model ids auto-route to the cheapest capable model.
| Framework | Endpoint | Auth header |
|---|---|---|
| OpenAI SDK / LangChain / etc. | POST /v1/chat/completions | Authorization: Bearer fa-… |
| Claude Code · Claude Agent SDK | POST /v1/messages | x-api-key: fa-… |
| Codex CLI (v0.132+) | POST /v1/responses | Authorization: Bearer fa-… |
All three accept tools, return tool calls, and accept tool results on follow-up turns, in both streaming and non-streaming modes — so multi-step agents complete real work, not just text.
Anthropic Messages
Anthropic-style clients use POST /v1/messages with the key as an x-api-key header. Full tool support: Anthropic tools (input_schema) and tool_choice are translated to the router's function-calling; responses return tool_use content blocks with stop_reason: "tool_use", and tool_result blocks on the next turn continue the loop. Streaming emits protocol-correct Anthropic SSE (incl. input_json_delta for tool input). Point Claude Code at it with ANTHROPIC_BASE_URL.
curl https://api.flowaiapi.com/v1/messages \
-H "x-api-key: fa-..." -H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"auto","max_tokens":256,
"tools":[{"name":"get_weather","description":"Get weather",
"input_schema":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}],
"messages":[{"role":"user","content":"Weather in Tokyo? Use the tool."}]}'
# -> stop_reason: "tool_use", content: [{"type":"tool_use","name":"get_weather","input":{"city":"Tokyo"}}]OpenAI Responses
POST /v1/responses implements the OpenAI Responses API, which the current Codex CLI requires. It accepts input (a string or an array of message / function_call / function_call_output items), instructions, flattened tools, tool_choice, and max_output_tokens, and returns output items (message with output_text, and function_call). Streaming emits the Responses event set (response.created → output_item.added → output_text.delta / function_call_arguments.delta → response.completed).
curl https://api.flowaiapi.com/v1/responses \
-H "Authorization: Bearer fa-..." -H "Content-Type: application/json" \
-d '{"model":"auto","max_output_tokens":256,
"tools":[{"type":"function","name":"get_weather","description":"Get weather",
"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}],
"input":"Weather in Tokyo? Use the tool."}'
# -> output: [{"type":"function_call","name":"get_weather","arguments":"{\"city\":\"Tokyo\"}"}]For Codex, set the base URL to https://api.flowaiapi.com/v1 and your fa-… key.
Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid API key. |
402 | Membership inactive, or insufficient balance — top up to continue. |
429 | Rate limit or per-key budget exceeded — back off and retry (honor Retry-After). |
502 | All capable providers failed for this request. |