docketrouter
DocumentationBrowse
API reference

Apps and attribution

One key often serves several products: a drafting tool, an intake bot, a nightly batch job. Attribution lets each request say which one it came from, so the usage log, the CSV export, the daily rollups and GET /apps can all be broken down by app without minting a key per product.

The headers are the OpenRouter convention plus one DocketRouter addition, so a client that already sends HTTP-Referer and X-Title upstream is attributed here with no change.

POST/chat/completionskey or sessionSend the headers here.
GET/appskey or sessionSpend per app.
GET/usage?app=key or sessionRows for one app.
GET/usage/daily?group=appkey or sessionPer-day rollups split by app.

The three request headers

HeaderBecomesRules
HTTP-Refererapp_urlMust parse as an http or https URL, otherwise it is dropped. A plain Referer header is accepted as a fallback. Trimmed to 512 characters.
X-Titleapp_nameThe display name of the calling app. Control characters are stripped, whitespace trimmed, and the result is cut at 80 characters.
X-DocketRouter-Appapp_nameSame meaning as X-Title and wins when both are sent. Use it when you cannot control what an SDK puts in X-Title.
  • A URL with no name attributes to its hostname, so HTTP-Referer: https://intake.example.com/new on its own gives app_name: "intake.example.com".
  • A name with no URL is fine. Most server-side jobs send only X-DocketRouter-App.
  • No usable header at all leaves both fields null, and the request lands in the unattributed bucket described below.
  • The headers are read on every call, so one key can carry different apps on different requests.
curl https://docketrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer dr-…" \
  -H "content-type: application/json" \
  -H "HTTP-Referer: https://intake.example.com" \
  -H "X-Title: Intake Bot" \
  -d '{
    "model": "deepseek/deepseek-v4-flash",
    "messages": [{ "role": "user", "content": "Under TRCP 21a, when is service by email complete?" }]
  }'

# X-DocketRouter-App takes precedence over X-Title when both are present
curl https://docketrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer dr-…" \
  -H "content-type: application/json" \
  -H "X-DocketRouter-App: nightly-batch" \
  -d '{ "model": "deepseek/deepseek-v4-flash", "messages": [{ "role": "user", "content": "…" }] }'
Attribution is a label, not a boundary

Anyone holding the key can send any app name. Use attribution to split a bill or a dashboard, and use separate keys with their own caps when two products must not be able to spend each other's budget. See limit_reset for per-key period caps.

Where the app shows up

On the response

The parsed attribution is echoed back as docketrouter.app on the JSON response and on the first chunk of a stream, so you can confirm what was recorded without reading the log.

docketrouter.app
"docketrouter": {
  "request_id": "req_f60437cb80a541d8912c",
  "app": { "name": "Intake Bot", "url": "https://intake.example.com" },
  …
}

On every usage row

Each row in GET /usage, GET /usage/:request_id and GET /generation?id= carries app_name and app_url. The CSV export puts the same two columns right after key_name. The per-generation view additionally groups them as app: { name, url }.

one row, abridged
{
  "request_id": "req_f60437cb80a541d8912c",
  "key_name": "prod-web",
  "app_name": "Intake Bot",
  "app_url": "https://intake.example.com",
  "model": "deepseek/deepseek-v4-flash",
  "cost_usd": 0.00007482,
  …
}

Filtering usage by app

GET /usage and GET /usage/daily both take an app parameter, an exact match on app_name. The usage envelope also carries by_app, a whole-scope rollup per app, and echoes the filter back under filters.app. GET /usage/daily?group=app splits each day's bucket by app.

filters
# rows for one app
GET https://docketrouter.ai/api/v1/usage?app=Intake%20Bot&from=2026-08-01

# one bucket per day per app, last 30 days
GET https://docketrouter.ai/api/v1/usage/daily?group=app

# the same as CSV
GET https://docketrouter.ai/api/v1/usage/daily?group=app&format=csv

The full parameter tables are on Usage and billing.

GET /apps

One row per app, sorted by spend, plus one bucket for everything unattributed.

GET/appskey or session
ParameterValuesNotes
fromISO timestampOnly usage at or after this time is counted.
toISO timestampOnly usage before this time is counted.
owneraccount idAdmin token only. Narrows the platform-wide view to one account.

A key sees only its own rows; a signed-in session sees every key on the account. Requests with no attribution are never mixed into data; they are summed into the single unattributed row, which is null when there are none.

200 OK
{
  "data": [
    { "name": "Intake Bot", "url": "https://intake.example.com",
      "requests": 412, "cost_usd": 0.0311, "prompt_tokens": 501220, "completion_tokens": 88410,
      "last_used": "2026-08-27T18:25:19.695Z" },
    { "name": "nightly-batch", "url": null,
      "requests": 96, "cost_usd": 0.0074, "prompt_tokens": 120050, "completion_tokens": 20115,
      "last_used": "2026-08-27T03:00:41.008Z" }
  ],
  "unattributed": { "name": null, "url": null,
                    "requests": 3, "cost_usd": 0.0002, "prompt_tokens": 3900, "completion_tokens": 610,
                    "last_used": "2026-08-20T11:02:55.117Z" }
}
FieldMeaning
name, urlThe attributed app. url is the most recently seen URL for that name.
requests, cost_usdRequest count and billed spend in the window.
prompt_tokens, completion_tokensToken totals in the window.
last_usedWhen this app last made a request.
curl "https://docketrouter.ai/api/v1/apps?from=2026-08-01" -H "Authorization: Bearer dr-…"

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.