DocumentationBrowse
Usage and billing
Every request through the gateway is logged against the key that made it. A key sees only its own rows; a signed-in session sees every key on the account. The log is the audit trail for cost, latency, grounding and fabrication, and it is where the verification report lives after the response is gone.
GET /usage
| Parameter | Values | Notes |
|---|---|---|
from | ISO timestamp or date | Inclusive lower bound on created_at. |
to | ISO timestamp or date | Upper bound. |
model | model id | Exact match. |
app | app name | Exact match on app_name, the value set by X-DocketRouter-App, X-Title or the referrer's hostname. See Apps and attribution. |
key | key id | Restrict to one key. An API key may only pass its own id (403 otherwise); a signed-in session may pass any key on its account (404 if the id belongs to no key there). |
status | ok or error | Errors here are upstream failures, not rejected requests. See the note below. |
juiced | 1 or 0 | Whether the grounding layer ran. |
limit | integer | Defaults to 100. |
offset | integer | Page with next_offset until it is null. |
format | csv | Returns text/csv with a filename attachment header. Up to 5,000 rows, and it ignores limit and offset. |
The envelope
| Field | Meaning |
|---|---|
totals | Aggregates over everything in scope, not just this page: requests, cost, prompt_tokens, completion_tokens, latency (mean ms), juiced and fakes_caught. |
by_model | Per model: requests, cost, tokens. |
by_key | Per key rollup. Present only for a signed-in session, absent for an API key. |
by_app | Per attributed app, over the whole scope: app_name, app_url (the most recently seen URL for that name), requests, cost, prompt_tokens, completion_tokens, last_used. One row has app_name: null for unattributed requests. Like totals and by_model, it honours from and to but not the other filters. |
data | The rows, newest first. |
total, limit, offset, next_offset | Paging. next_offset is null on the last page. |
filters | Echoes the filters applied to data: from, to, model, app, key, status, juiced. Fields you did not pass are omitted. |
{
"totals": { "requests": 7, "cost": 0.00135336, "prompt_tokens": 8696,
"completion_tokens": 1891, "latency": 15312.71,
"juiced": 7, "fakes_caught": 0 },
"by_model": [ { "model": "deepseek/deepseek-v4-flash", "requests": 7,
"cost": 0.00135336, "tokens": 10587 } ],
"by_app": [ { "app_name": "Intake Bot", "app_url": "https://intake.example.com",
"requests": 5, "cost": 0.00101, "prompt_tokens": 6200,
"completion_tokens": 1300, "last_used": "2026-08-27T18:25:19.695Z" },
{ "app_name": null, "app_url": null, "requests": 2, "cost": 0.00034336,
"prompt_tokens": 2496, "completion_tokens": 591,
"last_used": "2026-08-26T09:11:02.410Z" } ],
"data": [ … ],
"total": 7, "limit": 5, "offset": 0, "next_offset": 5,
"filters": { "from": "2026-08-01", "status": "ok" }
}A request rejected before the model call (400, 401, 403, 413, 422, 429) writes no row at all. The log covers requests that reached a provider, whether they succeeded (status: "ok") or failed there (status: "error"). If you need a count of client errors, count them in your own client.
Row fields
{
"id": 75,
"request_id": "req_f60437cb80a541d8912c",
"created_at": "2026-08-27T18:25:19.695Z",
"key_id": "672f0e35-…", "key_name": "docs-v2",
"app_name": "Intake Bot", "app_url": "https://intake.example.com",
"model": "deepseek/deepseek-v4-flash",
"provider": "Baidu",
"juiced": 1, "status": "ok",
"prompt_tokens": 514, "completion_tokens": 120, "cached_tokens": 0,
"cost_usd": 0.00007482, "upstream_cost_usd": 0.00005985252,
"latency_ms": 2784, "retrieval_ms": 952,
"sources_n": 3, "citations_checked": 0, "citations_not_found": 0,
"or_generation_id": "gen-1787855117-9x3UcZvNvaKglZLhzoyD",
"synced_at": "2026-08-27T18:25:19.695Z",
"error": null,
"verification": { "checked": [], "fabricated": [], "unverified": [], "revised": false },
"has_content": false
}| Field | Meaning |
|---|---|
created_at | When the row was written, which is when the request finished. |
request_id | The req_… handle also returned in the response header. |
key_id, key_name | Which key made the request. |
app_name, app_url | Which app made the request, from the attribution headers. Both null when none were sent. See Apps and attribution. |
model | The model you asked for. |
provider | The upstream provider that actually served it, once reconciled. Two identical requests can differ. |
juiced | 1 if the grounding layer ran, 0 if you sent juice: false. |
status | ok or error. |
prompt_tokens, completion_tokens | Native token counts. Prompt tokens include the injected authorities. |
cached_tokens | Prompt tokens served from the provider prompt cache. |
cost_usd | What you are billed for this request. |
upstream_cost_usd | What the provider charged. The ratio is the markup, currently 1.25, so you can audit it. |
latency_ms | Wall clock for the whole request, retrieval and verification included. |
retrieval_ms | Time spent in the grounding layer before the model call. |
sources_n | Retrieved rules plus cases injected. 0 on a degraded request, which makes degradation countable after the fact. |
citations_checked | Reporter citations found in your prompt and checked. |
citations_not_found | Of those, how many came back not found. This is the fabrication counter. |
verification | The full answer-side report: checked, fabricated, unverified, revised. |
or_generation_id | The upstream generation id, used for reconciliation. |
synced_at | When this row was last reconciled against the provider record. |
error | The upstream failure message, kept here rather than returned in the 502. |
has_content | Whether the prompt and answer were stored. False unless log_content is on for the key. The list endpoint never returns the content itself. |
GET /usage/:request_id and GET /generation?id=
Everything about one request, including what the response no longer has.
Same fields as a list row, plus two more and a set of grouped views of the same values. This is where a request id from a support ticket turns into an answer. GET /generation?id=req_… is the same endpoint with the id in the query string, matching OpenRouter's GET /generation so a client written against it works unchanged; the two bodies are byte for byte identical, and GET /generation without an id is a 400.
| Field | Meaning |
|---|---|
content | The last 6 messages and the answer, or null. Only present when the key had log_content on at request time. |
openrouter_generation | The provider's own record for this generation: native token counts, cache hits, moderation and generation latency, finish reason, data region, and billed_usd. Fetched live, and null when it is not available. |
Additive fields
Nothing was renamed or removed; the flat fields stay exactly as they are on a list row. These objects group the same values so a client can read the shape it wants.
| Field | Contents |
|---|---|
app | { name, url }, the attributed app or two nulls. |
price_usd | The DocketRouter price for this request. Same value as cost_usd. |
tokens | { prompt, completion, cached }. |
latency | { total_ms, retrieval_ms }. |
grounding | { sources_n, citations_checked, citations_not_found, reranked, verification }. |
key | { id, name }, the key that made the request. |
timestamps | { created_at, synced_at }. |
{
"request_id": "req_f60437cb80a541d8912c",
"model": "deepseek/deepseek-v4-flash", "status": "ok",
"prompt_tokens": 514, "completion_tokens": 120, "cost_usd": 0.00007482,
…
"app": { "name": "Intake Bot", "url": "https://intake.example.com" },
"price_usd": 0.00007482,
"tokens": { "prompt": 514, "completion": 120, "cached": 0 },
"latency": { "total_ms": 2784, "retrieval_ms": 952 },
"grounding": { "sources_n": 3, "citations_checked": 0, "citations_not_found": 0,
"reranked": null, "verification": { … } },
"key": { "id": "672f0e35-…", "name": "docs-v2" },
"timestamps": { "created_at": "2026-08-27T18:25:19.695Z", "synced_at": "2026-08-27T18:25:19.695Z" }
}{
"messages": [{ "role": "user", "content": "One line: what does Rule 12(b)(6) test?" }],
"answer": "Rule 12(b)(6) tests whether the complaint states a claim upon which relief can be granted."
}Fetching a request also opportunistically fills in a missing cost from the provider record, so a row that had no cost at write time will have one after you read it.
POST /usage/sync
Reconcile recent rows against the provider's per-generation truth.
curl -X POST "https://docketrouter.ai/api/v1/usage/sync?limit=50" -H "Authorization: Bearer dr-…"
{ "synced": 12, "unresolved": 0, "remaining": 0 }- Only rows that have not been reconciled are touched, so the call is safe to repeat and cheap when there is nothing to do.
limitdefaults to 50 and is capped at 200.- It fills in exact upstream cost, the provider that served the request, native token counts and cache hits.
unresolvedcounts rows whose provider record could not be fetched. They stay eligible for the next run.- Run it on a schedule (hourly is plenty) if you invoice from this data. Treat the synced value as the billable one.
GET /usage/daily
One bucket per UTC calendar day, for charts and invoices.
| Parameter | Values | Notes |
|---|---|---|
from | ISO timestamp or date | Defaults to 30 days ago at 00:00Z. |
to | ISO timestamp or date | Defaults to now. |
model | model id | Exact match. |
app | app name | Exact match on app_name. |
key | key id | Same rules as on GET /usage: a key may only name itself, a session any key on its account. |
group | model or app | Splits each day into one row per model or per app. Anything else is a 400. Omit it for one row per day. |
format | csv | Returns text/csv with a filename attachment header and the columns below, in that order. |
{
"from": "2026-07-28T00:00:00.000Z", "to": "2026-08-27T18:40:02.113Z", "group": "app",
"data": [
{ "day": "2026-08-26", "app_name": "Intake Bot", "requests": 41,
"prompt_tokens": 50210, "completion_tokens": 8830, "cost_usd": 0.0031, "upstream_cost_usd": 0.00248 },
{ "day": "2026-08-26", "app_name": null, "requests": 2,
"prompt_tokens": 2496, "completion_tokens": 591, "cost_usd": 0.00034, "upstream_cost_usd": 0.00027 },
{ "day": "2026-08-27", "app_name": "Intake Bot", "requests": 37, … }
],
"totals": { "requests": 80, "prompt_tokens": 98006, "completion_tokens": 17102,
"cost_usd": 0.00612, "upstream_cost_usd": 0.0049 }
}dayis the UTC date the row was written,YYYY-MM-DD. Days with no traffic are absent, not zero; fill them in on your side if a chart needs a continuous axis.modelis present only withgroup=model,app_nameonly withgroup=app. Both are empty columns in the CSV when not grouped.totalssumsdataand is the same however you group.
day,model,app_name,requests,prompt_tokens,completion_tokens,cost_usd,upstream_cost_usd
GET /credits
The API credits behind the account, in cents and in dollars.
A key reports on the account it is attached to; a signed-in session reports on its own account; the admin token must name an account with ?owner= and gets a 400 without it. The call reads the ledger and never touches a model, so it costs nothing.
{
"data": {
"owner": "user_2p…",
"balance_cents": 800, "balance_usd": 8, // purchased minus refunded
"total_purchased_cents": 1000, "total_purchased_usd": 10,
"total_refunded_cents": 200,
"total_used_usd": 0.5, // metered spend across every key on the account
"pending_cents": 0, "pending_usd": 0, // unexpired Monero invoices not yet confirmed
"trial_allowance_usd": 10,
"billing_enabled": true,
"monero": false
}
}- A key minted by the operator with no account attached gets
owner: null, zeros everywhere excepttotal_used_usd(that key's own spend), and anotesaying the key limit alone caps it.monerois omitted on that branch. balance_centsis what was paid in and not refunded; it does not subtract usage. Readtotal_used_usdnext to it, or useavailable_centsonGET /billing/balance, which does.- How credits are bought, and how a top-up raises every key's cap, is on Keys and identity.
Export and client billing
format=csv returns a flat file with a stable column order and a filename attachment header. It deliberately omits verification and any content, so it is safe to hand to a finance system.
created_at,request_id,key_name,app_name,app_url,model,provider,juiced,status, prompt_tokens,completion_tokens,cached_tokens,cost_usd,upstream_cost_usd,latency_ms, retrieval_ms,sources_n,citations_checked,citations_not_found,or_generation_id,error 2026-08-27T18:25:19.695Z,req_f60437cb80a541d8912c,docs-v2,Intake Bot,https://intake.example.com, deepseek/deepseek-v4-flash,Baidu,1,ok,514,120,0,0.00007482,0.00005985252,2784,952,3,0,0,gen-1787855117-…,
# recent requests, newest first curl "https://docketrouter.ai/api/v1/usage?limit=100" -H "Authorization: Bearer dr-…" # filters curl "https://docketrouter.ai/api/v1/usage?from=2026-08-01&status=ok&juiced=1&limit=100&offset=0" \ -H "Authorization: Bearer dr-…" # rows for one app, or one key on the account curl "https://docketrouter.ai/api/v1/usage?app=Intake%20Bot" -H "Authorization: Bearer dr-…" curl "https://docketrouter.ai/api/v1/usage?key=672f0e35-…" -H "Authorization: Bearer dr-…" # CSV export (up to 5000 rows) curl "https://docketrouter.ai/api/v1/usage?format=csv" -H "Authorization: Bearer dr-…" -o usage.csv # per-day rollups, split by app curl "https://docketrouter.ai/api/v1/usage/daily?group=app&from=2026-08-01" -H "Authorization: Bearer dr-…" # one request in full, either spelling curl "https://docketrouter.ai/api/v1/usage/req_f60437cb80a541d8912c" -H "Authorization: Bearer dr-…" curl "https://docketrouter.ai/api/v1/generation?id=req_f60437cb80a541d8912c" -H "Authorization: Bearer dr-…" # API credits behind the account curl "https://docketrouter.ai/api/v1/credits" -H "Authorization: Bearer dr-…" # reconcile (safe to repeat) curl -X POST "https://docketrouter.ai/api/v1/usage/sync?limit=50" -H "Authorization: Bearer dr-…"
A worked per-matter billing export, keyed on session_id, is in Recipes.
Pricing
Per token, no minimums, no subscription: the upstream provider price times 1.25. That same multiple is applied to the prices in the model catalog and to usage.cost on each response, so the number you were quoted, the number in the response and the number in the log all agree. You can check it on any row: cost_usd / upstream_cost_usd is the markup.
- Prompt tokens include the injected authorities, so a grounded request costs more input tokens than the text you sent. Input tokens are the cheap ones.
- The revise pass, when it runs, is billed in the same request.
- Each key carries a hard spend cap enforced upstream, so a runaway loop can cost at most that key's cap. A key on a limit_reset schedule is additionally capped per day, week or month, and answers 402 before any model call once the period is spent.
cost_usdis written from provider metadata at request time and refined byPOST /usage/sync.
Something here wrong or missing? Mail hello@docketrouter.ai with the request_id and we will fix the docs or the API, whichever is broken.