CROWBOTDOCS
DISCORD CHAT LOG IN

API reference

crowbot speaks the OpenAI chat-completions dialect. Anything that can talk to OpenAI can talk to crowbot by changing two settings: the base URL and the API key.

base URL   https://api.crowbot.sh/v1
auth       Authorization: Bearer <your 16-digit account number>

Your account number is your API key. There is no separate key to generate. Spaces are accepted, so you can paste it exactly as it was shown to you.

Where things live

Three origins, each serving only its own paths; a path asked of the wrong one is a plain 404.

origin serves
https://api.crowbot.sh /v1/*, /api/me and /api/plans — what an agent configures and reads
https://chat.crowbot.sh the web chat, signup, funding (/api/funding, /checkout), plans, pairing
https://crowbot.sh these docs, /llms.txt, the installer

GET https://api.crowbot.sh/api/me returns site and api, so a client given one origin can find the others.

Endpoints

method path origin auth what
POST /v1/chat/completions https://api.crowbot.sh key, paid balance chat completions — streaming, tool calling, reasoning_effort
GET /v1/models https://api.crowbot.sh none live models with limits and per-direction rates
GET /api/me https://api.crowbot.sh, https://chat.crowbot.sh key or chat cookie balance, spend, request count, key hint, can_spend, the active pass (plan, scope, ends_at, allowance_bp, refills) or null, the balance-fallback switch, and the site/api origins
POST /api/me/balance-fallback https://api.crowbot.sh, https://chat.crowbot.sh key or chat cookie body {"on": bool}: whether the balance pays once a plan's allowance is spent
GET /api/me/usage https://api.crowbot.sh, https://chat.crowbot.sh key or chat cookie this account's own spend, by model and by day, over days (default 30, 0 is all time): {days, models: [{model, label, requests, prompt_tokens, completion_tokens, cached_tokens, spent_usd}], series: [{day, requests, spent_usd}]}, a day without usage as zeros
GET /api/me/usage/log https://api.crowbot.sh, https://chat.crowbot.sh key or chat cookie one row per request, paged 50 at a time. page, sort (ts/model/tokens/cost), dir (asc/desc), days. totals covers the whole window, never the page
GET /api/me/payments https://api.crowbot.sh, https://chat.crowbot.sh key or chat cookie this account's payments, newest first, paged 50 at a time (page): {payments: [{at, usd, kind, plan_id, credited}], page, pages, total, totals: {credited, credited_usd}}; plan_id names the plan a settled payment bought, else null; totals covers every payment, never the page
GET /api/me/keys https://api.crowbot.sh, https://chat.crowbot.sh key or chat cookie the keys on this account (paired machines)
POST /api/me/keys/{ref}/revoke https://api.crowbot.sh, https://chat.crowbot.sh key or chat cookie revoke one key by its ref; the key you are using is refused (409)
POST /api/me/delete https://api.crowbot.sh, https://chat.crowbot.sh key or chat cookie close this account for good; body {account_number}; any balance is forfeited
POST /api/pair/start https://api.crowbot.sh, https://chat.crowbot.sh none (per-address cap) open a pairing: {device_code, user_code, ttl, interval}; body {"hostname", "client"} optional
GET /api/pair/{device_code} https://api.crowbot.sh, https://chat.crowbot.sh none (per-address cap) poll: 202 pending, 200 {status: ready, api_key} once, 410 expired
GET /pair https://chat.crowbot.sh none the page a person types the user_code into
POST /pair https://chat.crowbot.sh chat cookie the same page, showing Approve for a typed code
POST /api/pair/approve https://api.crowbot.sh, https://chat.crowbot.sh chat cookie approve {user_code} (mints a key for that machine) or {user_code, deny: true}
POST /api/signup https://chat.crowbot.sh none + X-Pow (kind signup) mint an account: {account_number, formatted, api_base}
GET /api/pow https://chat.crowbot.sh none for ?kind=signup; key or cookie for chat a proof-of-work challenge {challenge, bits, ttl}
POST /api/chat https://chat.crowbot.sh key or chat cookie + X-Pow the web chat's own streaming door; agents use /v1/chat/completions
GET /api/models https://chat.crowbot.sh none the chat picker's rows, coming-soon ones included with their status (/v1/models is live only): {models, default, efforts, lines, pieces, uploads: {per_message, file_max_bytes, max_px, picture_max_bytes}, vision: {images_max, step, max_px}}; default is the default model's id, efforts the reasoning_effort rungs, lines each line's label by id, pieces each media tool's caps and its failed words by tool name
POST /api/me/billing-portal https://api.crowbot.sh, https://chat.crowbot.sh key or chat cookie a session in the processor's portal for the cards it keeps for you {url}
GET /api/live https://chat.crowbot.sh none server-sent events: each topic named (?topics=tokens,news, required) now, then on every change
GET /api/live/{topic} https://chat.crowbot.sh, https://crowbot.sh none a live topic's latest payload; tokens is {total, rate}: every token served, all time, and tokens a second over the last minute
POST /api/tools/generate_image https://chat.crowbot.sh key + X-Pow the web chat's image tool: queues {job, price_micros, retry_ms}, billed per picture made when done; not offered on /v1
GET /api/tools/generate_image/{job} https://chat.crowbot.sh key a picture request's {status, retry_ms}, with charged_microdollars and made once done or message once failed
GET /api/tools/generate_image/{job}/image/{k} https://chat.crowbot.sh key the k-th finished picture (from 0), fetched for each download and never stored; 410 media_gone once the vendor has dropped it
POST /api/tools/generate_video/quote https://chat.crowbot.sh key the web chat's video tool, priced: {price_micros, funded_by, payable, seconds, aspect, resolution} for the submit's body; nothing is held
POST /api/tools/generate_video https://chat.crowbot.sh key + X-Pow the web chat's video tool: a submit with expect_micros queues {job}, billed by the second when done; not offered on /v1
GET /api/tools/generate_video/{job} https://chat.crowbot.sh key a clip's {status, retry_ms}, with charged_microdollars once done or message once failed: submitting, queued, rendering, done, failed or expired
GET /api/tools/generate_video/{job}/video https://chat.crowbot.sh key a finished clip's mp4, fetched for each download and never stored; 410 media_gone once the vendor has dropped it
GET /api/funding https://chat.crowbot.sh none what a top-up may be and how to pay for it {min_usd, max_usd, pay_with, rails, wallet}; wallet ({name, kind}) is the row a plan the balance covers is paid from
GET /api/plans https://api.crowbot.sh, https://chat.crowbot.sh none, or key or chat cookie the plans on sale to this account; {plans: []} while none is
POST /api/plans/{plan_id}/buy https://api.crowbot.sh, https://chat.crowbot.sh key or chat cookie buy that plan's pass with credit already in the wallet; {ok, pass_id}, 402 when short
GET /api/me/subscription https://api.crowbot.sh, https://chat.crowbot.sh key or chat cookie the renewing agreement this account holds, or {subscription: null}
POST /api/me/subscription/cancel https://api.crowbot.sh, https://chat.crowbot.sh key or chat cookie stop renewing; the pass already paid for runs to its end
GET /checkout https://chat.crowbot.sh key or chat cookie ?usd=N for credit, ?plan=ID for a pass, &renew=1 for one that renews → {checkout_url} (JSON) or a 303 to the processor (browser)
GET /paid https://chat.crowbot.sh none where the processor sends a person back after paying
GET / https://chat.crowbot.sh, https://crowbot.sh none the landing page on the site origin, the web chat on the chat origin
GET /docs https://crowbot.sh none 301 to the first docs page
GET /docs/{name} https://crowbot.sh none one docs page, rendered for people and search engines
GET /robots.txt https://crowbot.sh none what search engines may crawl, and where the sitemap is
GET /sitemap.xml https://crowbot.sh none the public pages, from content/site/pages.yaml
GET /docs/{name}.md https://crowbot.sh none one docs page as markdown
GET /docs/opencode.json https://api.crowbot.sh, https://crowbot.sh none generated opencode provider config
POST /webhooks/{hook} https://crowbot.sh the rail's own signature a payment rail's callback, at the tail it declares
GET /healthz every origin none liveness, on every host
GET /privacy https://crowbot.sh none the Privacy page
GET /terms https://crowbot.sh none the Terms page
GET /acceptable-use https://crowbot.sh none the Acceptable use page
GET /refunds https://crowbot.sh none the Refunds page
GET /why https://crowbot.sh none the Why it's cheap page
GET /impressum https://crowbot.sh none the Impressum page
GET /claims https://crowbot.sh none the Claims page
GET /install.sh https://crowbot.sh none the opencode installer (bash)
GET /install.ps1 https://crowbot.sh none the opencode installer (PowerShell, Windows)
GET /llms.txt https://crowbot.sh none this document
GET /llms-full.txt https://crowbot.sh none every docs page inlined
GET /docs/opencode-plugin.js https://api.crowbot.sh, https://crowbot.sh none the plugin the installer drops in

/v1/chat/completions is a passthrough: your system message is sent to the model unchanged and stays first, and so are your tools, tool_choice and response_format. Your system prompt is never replaced, edited, reordered or moved behind anything of ours.

Some models add a short instruction block of their own alongside it — formatting or refusal style. Your prompt keeps its exact bytes and its position; ours goes after it.

Request

json{
  "model": "glm-5.3-flash-crow",
  "messages": [
    {"role": "system", "content": "You are a careful assistant."},
    {"role": "user", "content": "Hello"}
  ],
  "max_tokens": 1024,
  "stream": true
}

An unrecognised model falls back to the default rather than erroring, because agentic clients often send names from other providers.

A model id we do recognise but are not serving returns 404 model_not_found and lists the live ids: falling back there would bill you for a model you did not ask for.

Sampling parameters are forwarded as you send them, including temperature and top_p. Which of them a model honours is up to its vendor.

reasoning_effort we do interpret. Send low, medium, high, xhigh or max: it overrides the model's configured default, any model takes any of them, and one without that tier gets its highest below it (xhigh and max reach a vendor with three tiers as high). Any other value is a 400 naming these. Reasoning tokens bill as output, so this is a spend control, not a quality slider.

Streaming

Set stream: true and read Server-Sent Events. Chunks are chat.completion.chunk objects terminated by data: [DONE], exactly as OpenAI emits them. Usage arrives on a final chunk with an empty choices array — you do not need to ask for it.

Reasoning models put their thinking trace in choices[].delta.reasoning_content, alongside the usual content. Clients that don't know the field simply ignore it.

Tool calling

Standard OpenAI shape in both directions:

json{
  "tools": [{"type": "function", "function": {
    "name": "bash",
    "description": "Run a shell command",
    "parameters": {"type": "object",
                   "properties": {"command": {"type": "string"}},
                   "required": ["command"]}
  }}],
  "tool_choice": "auto"
}

Replies carry message.tool_calls[] with function.arguments as a JSON string, and finish_reason: "tool_calls". Return results as {"role": "tool", "tool_call_id": …, "content": …} messages — one per call, as OpenAI expects. Parallel calls work; send every result before the next turn.

Streaming tool calls arrive as delta.tool_calls[] fragments. Concatenate function.arguments per index to rebuild the JSON.

Errors

Errors are {"error": {"message": …, "type": …}}. The type is the field to branch on — not the status, because a status can be ambiguous. A type worth sending again unchanged adds "retry": true.

status type meaning
400 bad_request the body was not a JSON object, or a parameter is invalid
400 bad_amount /checkout: not an amount to the cent within min_usd..max_usd
400 number_mismatch /api/me/delete: the account number typed is not this account's
400 cannot_renew /checkout?renew=1: the chosen payment method cannot hold a card to charge again
400 rail_not_accepted /checkout?plan= or /api/plans/{id}/buy: this plan does not take that way of paying (its payment_kinds)
401 invalid_api_key account number unknown or revoked
403 account_disabled the account is frozen (a card chargeback or refund); it can read its status but not spend or add funds
402 insufficient_balance out of funds, or this request's input alone costs more than the balance — top up
402 plan_allowance_spent the plan's allowance is spent and this account is set not to fall back on its balance
404 model_not_found a model we know but are not serving; the message lists the live ids
404 not_found no such page, key or route on this host
404 no_such_plan the plan named is not on sale to this account
404 no_subscription /api/me/subscription/cancel: this account has no renewing plan
405 method_not_allowed the path takes another method; Allow names it
409 cannot_revoke_current that key authenticated this request; revoke it from another
409 subscription_active /api/me/delete: a renewing plan is still active; cancel it first
409 price_changed the chat's video tool: the price moved since the quote it was sent with; quote it again
410 pair_expired the pairing code expired or was already used
410 media_gone a finished clip the vendor no longer holds; nothing was kept here to send instead
413 body_too_large over the 16 MiB request-body ceiling
428 pow_required proof of work missing or expired (chat and signup only, never /v1)
429 rate_limited back off; honour Retry-After when present
502 checkout_failed the payment processor did not answer
502 cancel_failed the payment processor did not answer a cancellation; retry shortly
404 no_customer /api/me/billing-portal: no card is saved yet; pay by card once
502 portal_failed /api/me/billing-portal: the payment processor did not answer; retry shortly
503 live_full /api/live: every live stream is taken; poll /api/live/{topic} and retry after Retry-After
503 live_warming /api/live/{topic}: the topic has not been read since the app started; retry shortly
503 tool_unavailable a tool is not configured or is temporarily off
503 checkout_unconfigured payments are not configured on this deployment
503 no_model nothing is live right now
503 no_provider that model's upstream is not configured
503 no_price a feed-priced model with no quote yet — retry shortly
503 price_fault a feed-priced model whose quote moved past our ceiling; off sale until a sane one lands
503 upstream_unavailable our problem, not yours — try another model
vendor's upstream_error the upstream refused: its own reason when your request is what it refused (a 4xx), our sentence for a throttle or a fault; its Retry-After if it sent one

Every /v1/chat/completions answer, error or not, carries an X-Request-Id header. Quote it when reporting a failure: it names the one record we keep of that call.

A streamed reply may open with an SSE comment line (:) and carry one during any pause: a keepalive, which every SSE client ignores. A failure after the stream has opened arrives as that same {"error": {"message": …, "type": …}} envelope in a data: frame, followed by data: [DONE].

An upstream vendor's own 401/402/403 is never relayed to you as-is. If our account with a vendor lapses you get a 503 upstream_unavailable: the fault is ours, not your key's or balance's.

Billing

Input, cached input and output bill at separate per-million rates — see Models. Every token sent upstream is billed, and cached input is discounted at the same ratio the upstream gives us.

A pass (see Plans) pays first for models in its scope; your balance pays for the rest. One request is paid by one of them.

A pass does not bill at the rates above. It carries an allowance, sized from what the plan cost, that requests draw down at what the model costs us and that refills continuously at the plan's rate — so it absorbs a burst and comes back over the day. Once the allowance cannot cover a request, that request is the balance's, unless you have said otherwise: POST /api/me/balance-fallback with {"on": false} caps you at the plan, and a request that would then need the balance is refused with 402 plan_allowance_spent and a message saying roughly when the allowance is back. GET /api/me shows the pass while you hold one: allowance_bp is how much of the allowance is left, in basis points of the whole, and refills says roughly when it is full again.

Non-streamed responses carry X-Balance-Microdollars with your remaining paid balance in microdollars (1e-6 USD); a request the pass paid for leaves it where it was. Streamed responses don't (headers precede the body): read /api/me or the final chunk's usage.

Every request is sized to whatever is paying for it before it is sent upstream: max_tokens (or max_completion_tokens) is capped to what the pass's remaining allowance or the balance can pay for after the estimated input, and a request whose input alone costs more than that is refused with 402 insufficient_balance before any vendor is called. Concurrency follows the payer too: each request in flight holds what it can cost (its input and its capped output), and together they may hold what the balance covers, or a pass's remaining allowance and a little past it, up to the parallel requests its plan sold. Past that comes a 429 rate_limited with Retry-After, which names the limit that was reached.

Plans

A plan is one price for a fixed number of days. Buying one gives the account a pass: an allowance that pays first for every model in the plan's scope, the way Billing describes, while the balance goes on paying for everything else. What is on sale is public:

GET https://chat.crowbot.sh/api/plans
Authorization: Bearer <your account number>     (optional)

The reply is {"plans": [...]}, one entry per plan you may buy:

json{"id": "starter", "label": "Starter", "tagline": "Hard working or hardly working?",
 "price_usd": 15, "days": 30, "concurrency": 2, "scope": ["frontier"],
 "features": ["Every frontier model", "Cancel any time"], "badge": null}

price_usd buys days days. concurrency is how many requests the pass runs at once; scope names the directions on the Models page it covers (frontier is the resold frontier models, crow our own). features and badge are the card's own words. A pass fixes its allowance in money when it is bought, so nothing on a plan moves when a vendor reprices.

The list is what your account may buy. Signed out, it is whatever is on general sale; while plans are in a closed beta it is empty unless your account is in it. {"plans": []} means nothing is on sale to you, not that the call failed.

Funding, programmatically

The chat UI does all of this with buttons; an agent can do it with four calls, all on https://chat.crowbot.sh except the last.

  1. Create an account. GET https://chat.crowbot.sh/api/pow?kind=signup returns {"challenge", "bits", "ttl"}. Find a nonce (≤ 40 chars) such that sha256("<challenge>:<nonce>") has at least bits leading zero bits, then POST https://chat.crowbot.sh/api/signup with header X-Pow: <challenge>.<nonce>. The reply is {"account_number", "formatted", "api_base"} — the only time the number is ever shown. Each signup from an address raises the next one's bits for a day, and signups are capped per address; a 428 means the work grew meanwhile: fetch again.
  2. Pick an amount. GET https://chat.crowbot.sh/api/funding → {"min_usd", "max_usd", "pay_with", "rails"} (rails: [{"name", "label", "renews"}]). Any amount in range is accepted, to the cent (usd=3.50), credited 1:1.
  3. Get an invoice. GET https://chat.crowbot.sh/checkout?usd=<amount> with Authorization: Bearer <number> returns {"checkout_url"} (a browser Accept: text/html request is redirected there instead). Payment is by crypto or card.
  4. Confirm. There is no payment-status endpoint. The processor's callback credits the wallet; poll GET https://api.crowbot.sh/api/me until balance_microdollars rises. Balances never expire.

Device pairing and keys

A machine can hold its own key, so a laptop can be revoked later without changing the account number. This is what the installer does; the raw flow:

  1. POST https://chat.crowbot.sh/api/pair/start (optional body {"hostname": "…", "client": "…"}; the key is named <client> @ <hostname>) → {"device_code", "user_code", "ttl", "interval"}. Unauthenticated, capped per address.
  2. Show the person the user_code and send them to https://chat.crowbot.sh/pair, where they type it and press Approve while signed in (programmatically: POST https://chat.crowbot.sh/api/pair/approve with the chat cookie and {"user_code"}, or {"user_code", "deny": true}).
  3. Poll GET https://chat.crowbot.sh/api/pair/<device_code> every interval seconds: 202 {"status": "pending"} while waiting, then 200 {"status": "ready", "api_key"} exactly once, then 410 pair_expired. Codes last 15 minutes.

GET /api/me/keys lists the account's live keys with a ref, name, hint, created_at and whether each is the one making the request; POST /api/me/keys/<ref>/revoke revokes one. The key that authenticated the request cannot be revoked (409 cannot_revoke_current): there is no recovery, so that click would be a permanent lockout.