Flynn API reference
Endpoints, limits, errors and response shapes of the Flynn API
Docs are being migrated. These are the getting-started guides. Product names have been updated to DigitalFrontier, but the SDK, CLI and config identifiers in code samples still reference their current names — we publish the surface that actually exists rather than a renamed one. The full API reference is being moved to its own documentation site.
Describes Flynn
f085af34ff1f, generated 2026-10-09. It is regenerated from the running service on every production deploy.
Compatibility and deprecation
What stays stable. These keep working as they do today:
POST /v1/chat/completions(OpenAI-compatible),POST /v1/messages(Anthropic-compatible) andGET /v1/models.- Authentication with an API key, sent as
Authorization: Bearerorx-api-key. - The error envelopes: the OpenAI shape (
error.message,error.type,error.param,error.code) on/v1/chat/completions, the Anthropic shape (type: "error",error.type,error.message) on/v1/messages, and the meaning of each documentedreason.
What can change without notice.
- Which models
GET /v1/modelslists. A model can be added or removed at any time, so check the list rather than assuming a model is there. - Which model the
flynnrouter picks for a request, and how it picks. - List prices, published as
pricingon each model inGET /v1/models. - Informational fields in a model's
flynnobject, such asdisplay_name,details,tagsandcost. - Anything added: new endpoints, response fields, headers or
reasonvalues. Your client should ignore what it does not recognise.
Breaking changes. Removing or renaming anything listed as stable, or changing what an existing reason means, gets 90 days' notice. A fix for a security problem may ship with less notice.
Where changes are announced. In the Flynn API changelog, with the date each change takes effect.
Questions. Write to support@digitalfrontier.so, and include the X-Flynn-Request-Id header from the response you are asking about.
Generated from the running Flynn service, so it describes what the API does today.
Before you start
Which host
Call the API at api.flynn.digitalfrontier.so. flynn.digitalfrontier.so is the web console, not the API: /v1/*
there answers with the console rather than JSON.
What the model id is
Each backend in GET /v1/models has its worker id as id; the underlying
model name is at .flynn.model. To find a model by name, filter on
.flynn.model, not .id. The router itself is listed as flynn (plus its
named versions), marked "flynn_router": true.
A model we run ourselves may also carry .flynn.display_name, the base model
without quantization or deployment suffixes (the label the console's Models page
shows), and .flynn.details with context_window, hosting, quantization.
They are labels only: pin a model by .id, which is unchanged. A model without
them is shown by its id.
Limits
Your own values are in limits on GET /api/v1/me and GET /v1/usage (see the
divergence note under Response shapes). This section documents the MECHANISM.
reason | status | resets |
|---|---|---|
rpm_window | 429 | at the next minute boundary |
tpm_window | 429 | at the next minute boundary |
monthly_token_cap | 429 | when the calendar month rolls over (UTC) |
monthly_cost_cap | 429 | when the calendar month rolls over (UTC) |
- Requests per minute: when neither your key nor your tenant sets one, the
default is
120. - Per-minute limits are counted per API replica, in process memory, so with several replicas the fleet-wide ceiling is a multiple of the number shown. The token limit is applied one request late: tokens are counted after a request completes.
- Monthly caps are enforced.
- Keys: there is no cap on the number of active API keys.
- A self-serve tenant starts with:
rpm_limit=30,tpm_limit=None,monthly_token_cap=5000000,monthly_cost_cap_usd=25.0.Nonemeans no limit.
Pricing
- Currency: USD. You are billed per token, with input and output tokens priced separately, in USD per 1M tokens.
- Per-model list prices are the
pricingobject on each model inGET /v1/models, called with your API key, and are shown on the console's Models page when you are signed in.pricing.promptis the input price andpricing.completionthe output price, as decimal strings in USD per 1M tokens. Prices can change, so read them from there rather than copying them. - The price shown is the price billed.
- A request sent to the router is billed at the list price of the model that served it.
- Models without a
pricingobject have no list price. They are billed at provider cost plus a margin. - Failed requests are not billed.
- Monthly caps are described under Limits above.
- Prepaid credit is added by top-up from the console, paid through Revolut hosted checkout, between 5 and 500 USD per top-up. Where top-ups are not yet offered, contact support@digitalfrontier.so to add credit.
Errors
Limit and capacity refusals carry a machine-readable reason in the error body
(and Retry-After where a retry can succeed). Switch on reason, not the message:
reason | meaning |
|---|---|
monthly_cost_cap | Your monthly spend allowance is used up. It resets with the calendar month. |
monthly_token_cap | Your monthly token allowance is used up. It resets with the calendar month. |
rpm_window | Too many requests in the current minute. Retry after the Retry-After header. |
shared_quota_exhausted | A shared upstream budget is spent. Retrying before Retry-After will not help. |
shared_quota_unavailable | The shared budget could not be checked, so the request was refused rather than risk overspending. |
tpm_window | Too many tokens in the current minute. Retry after the Retry-After header. |
upstream_failed | Every model backend that could serve the request failed. |
wall_clock_cap | The request ran past the gateway's time limit and was cancelled. |
worker_blocked | The backend for this model is busy. Retryable; honour Retry-After. |
Failed requests in GET /api/v1/me/requests carry an error_class:
error_class | meaning |
|---|---|
auth | Authentication or authorization failed. |
client_disconnect | The client closed the connection before the response finished. |
context_overflow | The request did not fit the model's context window. |
invalid_request | The request itself was malformed or unsupported. |
monthly_cap | Refused by a monthly token or spend cap. |
rate_limit | Refused by a per-minute request or token limit. |
shared_quota | Refused because a shared upstream budget was spent or unavailable. |
unclassified | Failed for a reason not yet classified. |
upstream_failed | The model backend failed. |
wall_clock_cap | Cancelled at the gateway's time limit. |
worker_blocked | The model backend was busy and admission refused the request. |
Messages raised directly in a customer route's handler or in the auth, scope and
limit checks every customer request passes, read from the source (messages raised
in deeper helpers are not scanned). … stands for a value filled in at run time:
| status | message |
|---|---|
| 400 | after must be strictly less than before (got after=…, before=…) |
| 400 | error_class must be one of …; got … |
| 400 | key must be a 32-char uuid hex |
| 400 | status=ok rows carry a NULL error_class; the two filters together always describe an empty window |
| 400 | status must be 'ok' or 'error' |
| 400 | amount_usd ∈ [5, 500] |
| 400 | bad after cursor |
| 400 | bad before cursor |
| 400 | days must be in [1, 90] |
| 400 | dep_limit must be in [1, 100] |
| 400 | expires_days ∈ [1, 365] |
| 400 | filter must be 'active', 'revoked', or 'all' |
| 400 | limit must be in [1, 100] |
| 400 | monthly_cost_cap_usd exceeds the tenant limit … |
| 400 | monthly_cost_cap_usd must be greater than 0; omit it to inherit the tenant cap |
| 400 | rpm_limit must be at least 1; omit it to inherit the tenant limit |
| 400 | rpm_limit … exceeds the tenant limit … |
| 400 | unknown scope(s): …. known scopes: … |
| 401 | invalid or missing API key |
| 403 | self-service keys are disabled |
| 403 | this endpoint is internal |
| 403 | this key is scope-restricted and the requested path is not available to any scope |
| 403 | this key lacks the '…' scope required for … |
| 404 | key not found |
| 404 | top-up not found |
| 409 | this top-up is …, not pending; it cannot be cancelled |
| 409 | this top-up's payment is already under way; it will be credited when it completes |
| 409 | top-ups are not available yet; contact support to add credit |
| 429 | monthly cost quota exceeded ($…/$…) |
| 429 | monthly token quota exceeded (…/…) |
| 429 | rate limit exceeded |
| 429 | token rate limit exceeded |
| 502 | the payment provider refused this top-up; nothing was charged |
| 503 | authentication temporarily unavailable |
| 503 | checkout is unavailable right now; nothing was charged, please try again |
| 503 | serving auth not configured |
Not listed: 0 message(s) built at run time, and 0 withheld because their text would name internal detail.
Endpoints
15 endpoints.
OpenAI- and Anthropic-compatible surface (API key)
| endpoint | method | auth | request shape | response shape | description |
|---|---|---|---|---|---|
/v1/chat/completions | POST | API key | no request body declared in the schema | read from handler (below) | Chat Completions |
/v1/messages | POST | API key | no request body declared in the schema | read from handler (below) | Create Message |
/v1/messages/count_tokens | POST | API key | no request body declared in the schema | read from handler (below) | Count Tokens |
/v1/models | GET | API key or dashboard session | no request body | read from handler (below) | List Models |
/v1/usage | GET | API key | no request body | read from handler (below) | My Usage |
Account surface (dashboard session)
| endpoint | method | auth | request shape | response shape | description |
|---|---|---|---|---|---|
/api/v1/me | GET | dashboard session | no request body | read from handler (below) | Whoami |
/api/v1/me/billing | GET | dashboard session | no request body · query: dep_cursor, dep_limit, day_cursor, days | read from handler (below) | My Billing |
/api/v1/me/billing/topup | POST | dashboard session | TopUpIn | read from handler (below) | Request Topup |
/api/v1/me/billing/topup/{intent_id}/cancel | POST | dashboard session | no request body declared in the schema | read from handler (below) | Cancel Topup |
/api/v1/me/keys | GET | dashboard session | no request body · query: limit, cursor, filter | read from handler (below) | My Keys |
/api/v1/me/keys | POST | dashboard session | MeKeyIn | read from handler (below) | Mint My Key |
/api/v1/me/keys/{key_id} | DELETE | dashboard session | no request body | read from handler (below) | Revoke My Key |
/api/v1/me/keys/{key_id}/rotate | POST | dashboard session | no request body declared in the schema | read from handler (below) | Rotate My Key |
/api/v1/me/models | GET | none (dashboard session optional) | no request body | read from handler (below) | My Models |
/api/v1/me/requests | GET | dashboard session | no request body · query: limit, before, after, key, status, error_class, cursor | read from handler (below) | My Requests |
Response shapes
The fields each endpoint returns.
GET /api/v1/me
tenant: object —id,name,createdlimits: object —monthly_token_cap,monthly_cost_cap_usd,rpm_limiteffective_limitsmonth
GET /api/v1/me/billing
balance_uactbalance_usdbilling_policypayment_referenceprovider_configuredprovider_statedeposits: array of objects —id,delta_uact,delta_usd,reason,ref,status,tsdeposits_next_cursorspend_by_day: array of objects —day,requests,total_uact,total_usdspend_next_day_cursor: object (may benull) —ledger: array of objects —id,delta_uact,delta_usd,reason,ref,status,ts
POST /api/v1/me/billing/topup
idamount_usdstatusproviderredirect_url
POST /api/v1/me/billing/topup/{intent_id}/cancel
idstatusreplayed
GET /api/v1/me/keys
keys: array of objects —id,prefix,name,created,revoked,last_usednext_cursorcounts
POST /api/v1/me/keys
idapi_keyprefixnamewarning
DELETE /api/v1/me/keys/{key_id}
idrevoked
POST /api/v1/me/keys/{key_id}/rotate
idapi_keyprefixrotated_from
GET /api/v1/me/models
objectdata
GET /api/v1/me/requests
requestsnext_cursor: object (may benull) —
POST /v1/chat/completions
- OpenAI Chat Completions response (proxied verbatim; SSE when
stream: true)
POST /v1/messages
- Anthropic Messages response (proxied verbatim; SSE when
stream: true)
POST /v1/messages/count_tokens
input_tokens
GET /v1/models
objectdataerror: object —message,type,code
GET /v1/usage
tenant: object (may benull) —id,name,createdtenant_idmonthlimits: object —monthly_token_cap,monthly_cost_cap_usd,rpm_limitdaily: array of objects —day,model,requests,prompt_tokens,completion_tokens,billed_usd
⚠️ /v1/usage and /api/v1/me return overlapping, non-identical data
Each describes the tenant that authenticated you, and that is not always the
same tenant. /v1/usage resolves it from your API key; /api/v1/me resolves it
from your dashboard login, provisioning a personal tenant on first sight. A key
that an operator minted for you belongs to a tenant you do not own, so the
two can describe different tenants for the same person.
They also do not agree on what they include. This reference states the difference rather than smoothing it over — papering over a real inconsistency would teach you something false about the API.
- only in
/api/v1/me:effective_limits - only in
/v1/usage:tenant_id,daily limitsonly in/api/v1/me: —limitsonly in/v1/usage: —
limits shares field names but not sourcing rules. On /v1/usage,
rpm_limit and monthly_cost_cap_usd come from your API KEY where the key sets
them and from its tenant otherwise, while monthly_token_cap is always the
tenant's. On /api/v1/me all of them are the tenant's. A key may be stricter
than its tenant, so the same field name can carry different numbers on
the two endpoints at the same moment, and nothing in either payload says which
rule produced a given value.
tenant_id on /v1/usage duplicates tenant.id and is transitional. Prefer
tenant.id.
Where a request-body cell says the schema does not declare one, the endpoint may still accept a body: the cell names the gap rather than implying it takes nothing.