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.

python
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 cost
curl
curl 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:

FieldValuesMeaning
route_strategyauto · 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_hintfree string (e.g. code)Helps the classifier choose a capable model.
verifytrue · "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 headerlatencyThe 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.
imagesimage_url partsBase64 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
"_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:

FieldMeaning
appWhich application sent the request (e.g. "support-bot").
agentWhich agent within the app.
run_idYour 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.

examples
# 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-…):

EndpointReturns
GET /v1/cortex/overview?days=7Spend, requests, action-rate and cost-per-completed-task trends.
GET /v1/cortex/routingPer-model: requests, measured action rate, spend — the routing evidence.
GET /v1/cortex/cachingPrompt-cache hit rates and savings.
GET /v1/cortex/contextYour 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 seeWhere it comes fromWhat 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/MFlow 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_reachedThe 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 1302The 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_idRunaway-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 progressingStuck-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 budgetA 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

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.

curl — Mandarin, auto-routed
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.mp3
curl — pinned Qwen3-TTS, Sichuan accent, with a tone instruction
curl 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.mp3

Video 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 — clip on the plan, then poll
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 Code
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
ToolReturns
search_modelsLive catalog matches: id, provider, context window, prices, verified caps.
get_live_pricesThe market book — clearing vs published list prices, savings %.
list_free_modelsThe canary-verified free models currently serving at $0.
about_flow_aiWhat 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_signupAgent 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_joinJoin 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

New team (no signup, no email)
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_signup

If 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.

Join an existing team (no key needed)
# 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.

Doorbell — one daemon, every harness
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 session

Why 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.

HarnessWakeStatus
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 sessionNo hooks yet: drain at the start and end of each task; the doorbell covers idle time.Documented.
Grok Bot / Cursor automationsYour 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 shapeHow 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 endpointRegister 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.
HumanThe 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

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

GuaranteeWhat it means
Exact serveA 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 loudUnknown model → 400. Pinned model can't serve → 503 model_unavailable. Never a fallback answer from a different model.
Fresh callsPinned requests bypass all response caching — each call really hits the model, so experiments reproduce.
Real-rate billingEach 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 — pinned request
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 — 3-model panel
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? ..."}]}'
response
{
  "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)

env
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

env
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+)

~/.codex/config.toml
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-...
FrameworkUse the blockEndpoint
Paperclip — claude_localAnthropic/v1/messages
Paperclip — opencode / OpenAI adapterOpenAI Chat/v1/chat/completions
Paperclip — codex_localResponses/v1/responses
HermesOpenAI Chat/v1/chat/completions
OpenClawAnthropic/v1/messages
Claude CodeAnthropic/v1/messages
Codex CLIResponses/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.

FrameworkEndpointAuth header
OpenAI SDK / LangChain / etc.POST /v1/chat/completionsAuthorization: Bearer fa-…
Claude Code · Claude Agent SDKPOST /v1/messagesx-api-key: fa-…
Codex CLI (v0.132+)POST /v1/responsesAuthorization: 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 — /v1/messages with a tool
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 — /v1/responses with a tool
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

StatusMeaning
401Missing or invalid API key.
402Membership inactive, or insufficient balance — top up to continue.
429Rate limit or per-key budget exceeded — back off and retry (honor Retry-After).
502All capable providers failed for this request.