docketrouter
DocumentationBrowse
API reference

Keys and identity

A dr- key is an inference and account-data credential. It is deliberately not a management credential: key creation, configuration, rotation and revocation require a signed-in browser session, so a leaked key cannot mint more keys or raise its own cap.

Keys are minted at /keys. The secret is shown once.

What a key can reach

EndpointAPI keySigned-in session
POST /chat/completionsyesyes
GET, POST /filesyesyes
DELETE /files/:idyesyes
GET /usage, /usage/daily, /appsyes (its own rows)yes (all keys on the account)
GET /usage/:request_id, /generationyesyes
POST /usage/syncyesyes
GET /auth/keyyesyes
GET /creditsyes (the account behind the key)yes
GET /billing/balance, /billing/autotopupno (401)yes
GET, POST /keysno (401)yes
GET, PATCH, DELETE /keys/:idno (403)yes, if you own the key
POST /keys/:id/rotateno (403)yes, if you own the key
401 and 403 mean different things on the key endpoints

The collection endpoints (/keys) answer 401 when you are not signed in, because they cannot tell whose keys you meant. The item endpoints (/keys/:id) answer 404 when the id does not exist and 403 when it exists and is not yours, so a 403 is a positive statement that the key belongs to someone else.

GET /auth/key

The cheapest way to confirm a key works and see what is left on its cap.

GET/auth/keykey or session
200 OK
{
  "data": {
    "label": "dr-e7i0UDF…lGsr",          // masked prefix, safe to display
    "name": "docs-v2",
    "limit": 5,                          // the cap in USD, lifetime or per period
    "limit_reset": "daily",              // none | daily | weekly | monthly
    "limit_reset_at": "2026-08-27T00:00:00.000Z",   // start of the current period
    "resets_at": "2026-08-28T00:00:00.000Z",        // start of the next one
    "spent": 0.5,                        // lifetime spend, USD
    "period_spent": 0.5,                 // spend since limit_reset_at
    "limit_remaining": 4.5,              // limit minus the counter the cap applies to
    "usage": 0.5,
    "usage_daily": null,                 // upstream figures, null when not available
    "usage_monthly": null,
    "rate_limit": { "requests": 120, "interval": "60s", "burst": 40 },
    "data_policy": "shared",
    "routing": "shared",
    "pin_provider": null,
    "provider_allow": [],
    "provider_order": [],
    "allow_fallbacks": true,
    "require_parameters": false,
    "jurisdiction": "tx",
    "log_content": false,
    "disabled": false,
    "created_at": "2026-08-27T18:21:13.549Z",
    "local": { "requests": 7, "cost": 0.5, "prompt_tokens": 8696, "completion_tokens": 1891,
               "latency": 15312.7, "juiced": 7, "fakes_caught": 0 }
  }
}
  • limit, limit_reset, spent, period_spent and limit_remaining are DocketRouter's own counters, the ones behind the 402 on a capped period. With limit_reset: "none", limit_remaining is limit minus spent; with a schedule it is limit minus period_spent. See limit_reset.
  • A due reset is rolled before the response is built, so period_spent is never stale.
  • rate_limit is the token bucket for this key: sustained requests per interval and the burst it can absorb. The same numbers come back on every chat response as X-RateLimit-* headers.
  • usage, usage_daily and usage_monthly are the upstream provider's figures when the host has management access, else usage falls back to spent and the other two are null.
  • data_policy through require_parameters is the full provider and data policy on the key, decoded, the same fields GET /keys/:id returns.
  • local is DocketRouter's own tally for this key, including fakes_caught, the running count of prompt citations that came back not found.
  • A signed-in session gets { "label": "session", "user_id": "…" } instead, and the admin token gets { "label": "admin", "is_admin": true }.
  • The call never touches a model, so it costs nothing. It is the right health check for a deploy.

Key settings

A key carries policy. Requests cannot downgrade it.

GET/keys/:idowner or admin
PATCH/keys/:idowner or admin
SettingValuesEffectWho can set it
name1 to 64 charactersLabel only. Appears on every usage row as key_name.Owner or admin
limit_usdnumberThe hard spend cap, enforced upstream and, with a limit_reset schedule, per period by the gateway. During beta an owner can move it anywhere up to $10.Owner up to the beta ceiling, admin higher
limit_resetnone, daily, weekly, monthlyHow often the cap in limit_usd starts over. Changing it starts a fresh period at once. Details below.Owner or admin
jurisdictiontx, ca, ny, fedThe default jurisdiction for every request on this key. A request can still override it per call.Owner or admin
log_contentbooleanOff by default. When on, the last 6 messages and the answer are stored on each usage row and returned by GET /usage/:request_id.Owner or admin
routingshared, no-train, zdr, pinnedProvider routing policy, applied on top of any request-level preferences.Admin
pin_providerprovider slugThe provider pinned when routing is pinned.Admin
data_policyshared, private_podA private-pod key may only call in-house local/ models, and the prompt never leaves the pod.Admin only. An owner cannot set or clear it.
PATCH /keys/:id
curl -X PATCH https://docketrouter.ai/api/v1/keys/672f0e35-… \
  -H "content-type: application/json" \
  -d '{ "jurisdiction": "fed", "log_content": true }'

{ "ok": true, "id": "672f0e35-…", "jurisdiction": "fed", "log_content": true,
  "data_policy": "shared", "routing": "shared", "pin_provider": null,
  "provider_allow": [], "provider_order": [], "allow_fallbacks": true, "require_parameters": false,
  "limit_reset": "none", "limit_reset_at": null, "resets_at": null,
  "spent_usd": 0.00135336, "period_spent_usd": 0.00135336, "remaining_usd": 0.99864664 }

The response echoes the top-level fields you changed, plus the decoded policy and the spend counters, which are always present. Read the whole state back with GET /keys/:id, which also returns the upstream cap and usage plus the 50 most recent requests on that key.

GET /keys/:id, abridged
{
  "id": "672f0e35-…", "name": "docs-v2",
  "prefix": "dr-e7i0UDF…lGsr",
  "limit_usd": 1, "disabled": false,
  "created_at": "2026-08-27T18:21:13.549Z",
  "log_content": false, "routing": "shared", "pin_provider": null,
  "data_policy": "shared", "jurisdiction": "tx",
  "limit_reset": "none", "limit_reset_at": null, "resets_at": null,
  "spent_usd": 0.00135336, "period_spent_usd": 0.00135336, "remaining_usd": 0.99864664,
  "openrouter": { "usage": 0.001082681, "limit": 1,
                  "limit_remaining": 0.998917319, "disabled": false },
  "local": { "requests": 7, "cost": 0.00135336, "prompt_tokens": 8696,
             "completion_tokens": 1891, "latency": 15312.7, "juiced": 7, "fakes_caught": 0 },
  "recent": [ … up to 50 usage rows … ]
}

Minting keys

POST/keyssession or admin
request and response
{ "name": "prod-web", "limit_usd": 10, "limit_reset": "weekly" }

{ "id": "672f0e35-cd2b-4580-af23-36f93538e4af",
  "name": "prod-web",
  "key": "dr-…",                 // shown once, never returned again
  "limit_usd": 10,
  "openrouter_backed": true,
  "limit_reset": "weekly",
  "limit_reset_at": "2026-08-24T00:00:00.000Z",   // Monday 00:00 UTC of this week
  "resets_at": "2026-08-31T00:00:00.000Z",
  "spent_usd": 0, "period_spent_usd": 0, "remaining_usd": 10 }
  • The secret in key is the only copy. There is no endpoint that returns it again.
  • limit_reset defaults to none. An unrecognised value is a 400.
  • Self-serve accounts are capped at 3 active keys and $10 per key during beta. A fourth request returns 429 with a message saying so, which is a quota answer rather than a rate-limit answer despite sharing the status code. An account with no spend headroom left gets a 402.
  • Each key carries its own hard upstream cap, set when it is minted. A runaway loop can cost at most that key's cap.
  • A failed mint never leaves a live spendable key behind.
Schema failures use the standard envelope

A bad body returns HTTP 400 with the first failing field in message, type: "invalid_request_error", and the full field map under details: {"error":{"message":"invalid limit_usd: Too small: expected number to be >0","type":"invalid_request_error","details":{"fieldErrors":{…},"formErrors":[]}}}

limit_reset: caps that start over

A daily, weekly or monthly budget on one key, enforced before the model is called.

By default limit_usd is a lifetime cap: once the key has spent it, the key is done until you raise it. Set limit_reset and the same number becomes a budget per period. The gateway keeps two counters on every key and applies the cap to whichever one the schedule says.

ValuePeriod startsPeriod endsThe cap applies to
noneneverneverspent_usd
daily00:00 UTC today00:00 UTC tomorrowperiod_spent_usd
weekly00:00 UTC on Monday00:00 UTC next Mondayperiod_spent_usd
monthly00:00 UTC on the 1st00:00 UTC on the 1st of next monthperiod_spent_usd

The counters

FieldMeaning
spent_usdLifetime spend on this key. Never resets. Survives a schedule change, a rotation, and a revoke.
period_spent_usdSpend since limit_reset_at. Zeroes when the period rolls. Equal to spent_usd when limit_reset is none.
remaining_usdlimit_usd minus the counter the cap applies to, floored at zero. null when limit_usd is null.
limit_reset_atUTC start of the current period. null with no schedule.
resets_atUTC start of the next period, when period_spent_usd goes back to zero. null with no schedule.

All five come back on every key response: GET /keys, GET /keys/:id, POST /keys, PATCH /keys/:id, and (as spent, period_spent, limit_remaining) on GET /auth/key.

What resets and what does not

  • Resets when the period rolls: period_spent_usd goes to zero and limit_reset_at moves to the new period start. The roll happens lazily on the next read or request, and the counters are computed as if it had already happened, so there is no window where a stale period blocks a key.
  • Resets when you change the schedule: setting limit_reset to any value, including the one it already has, starts a fresh period immediately: period_spent_usd zeroes and limit_reset_at re-anchors to the current period's start.
  • Never resets: spent_usd, the usage rows, the key's local totals, and the upstream lifetime cap behind the key. Switching a key back to none makes the cap apply to lifetime spend again, so a key that has already spent more than limit_usd in its life stops at once.

What a capped key gets

When period_spent_usd reaches limit_usd, POST /chat/completions answers 402 before any model call, so nothing is billed and no rate-limit token is wasted on the model side. The body names the schedule and the moment the cap frees up.

402 Payment Required
{
  "error": {
    "message": "key spend cap reached for this period; resets at 2026-08-28T00:00:00.000Z",
    "type": "payment_required_error",
    "limit_usd": 5,
    "period_spent_usd": 5.000213,
    "limit_reset": "daily",
    "resets_at": "2026-08-28T00:00:00.000Z"
  }
}
  • Do not retry a 402 in a loop. Sleep until resets_at, raise limit_usd, or move the workload to another key.
  • A key with limit_reset: none never gets this response from the gateway; its lifetime cap is enforced upstream, and exhausting it surfaces as an upstream refusal on the request.
  • A period can end slightly over the cap: the check runs before the call, so the request that crosses the line is served and counted.

API credits

Beyond the free trial, an account tops up its API credits; nothing is ever billed after the fact.

GET/creditskey or session
GET/billing/balancesession
POST/billing/checkoutsession
GET, PUT/billing/autotopupsession

Every self-serve account gets a free trial allowance with no card on file. Past that, API credits are prepaid: pay first, spend down from what you paid. Nothing is ever billed retroactively, and a key can never spend more than the credits behind it because its upstream cap is set from that balance, not the other way around. The balance a key or a session can read at any time is GET /credits, documented on Usage and billing.

  • Card, via Stripe Checkout. POST /billing/checkout returns a Stripe-hosted Checkout URL. Send exactly one of amount_cents, a preset (1000, 2500 or 10000, the $10 / $25 / $100 buttons), or amount_usd, a custom amount in whole dollars from 5 to 1000. Cents in the custom field, amounts outside the range, and both fields at once are rejected with a 400. The response carries the url and the amount_cents the session will charge.
  • Auto top-up. GET /billing/autotopup reads the settings and PUT /billing/autotopup changes them: enabled, threshold_usd (charge when available API credits fall below this; default 5, allowed 1 to 500) and amount_usd (what each top-up adds; default 25, allowed 10 to 500), all in whole dollars. Enabling it with no card on file saves the settings with enabled: false and returns a setup_url, a Stripe session that saves a card without charging it; the card is confirmed by webhook and auto top-up switches on. update_card: true returns a fresh setup_url without touching enabled. One charge is made per crossing of the threshold, never more than amount_usd, never while a previous charge is still pending, and three consecutive failed charges switch it off (disabled_reason says why; turning it back on resets the count). The response also reports card_on_file, brand and last4, pending, and available_cents, the figure compared against the threshold.
  • Monero. When enabled on the host, POST /billing/monero issues a BTCPay-backed invoice with the USD/XMR rate locked for 30 minutes. Credits post once the payment reaches the required confirmations.
  • After payment. The balance is credited exactly once per payment (a retried or duplicate delivery of the same payment never double-credits), and every active key's upstream cap is immediately recomputed from the new balance so the extra headroom is spendable right away, with no restart or re-mint needed. A refund debits the same balance back down and recomputes caps again.
checkout and auto top-up
POST https://docketrouter.ai/api/v1/billing/checkout
{ "amount_usd": 37 }
{ "url": "https://checkout.stripe.com/c/pay/cs_…", "amount_cents": 3700 }

PUT https://docketrouter.ai/api/v1/billing/autotopup
{ "enabled": true, "threshold_usd": 5, "amount_usd": 25 }
{ "enabled": false, "threshold_usd": 5, "amount_usd": 25, "card_on_file": false,
  "pending": false, "consecutive_failures": 0, "last_failure_at": null, "last_failure_reason": null,
  "disabled_reason": "no card on file", "available_cents": 800,
  "limits": { "threshold_min_usd": 1, "threshold_max_usd": 500, "amount_min_usd": 10, "amount_max_usd": 500 },
  "setup_url": "https://checkout.stripe.com/c/pay/cs_…" }   // save a card here; enabled flips on afterwards
Card top-ups may be off

While a host has not enabled card billing, POST /billing/checkout and PUT /billing/autotopup return 503 and the site shows the free trial allowance only. GET /billing/balance and GET /credits always report whether card billing is enabled on that host.

Purchases, refunds and metered spend are governed by the Terms of service.

Rotation and revocation

POST/keys/:id/rotateowner or admin
DELETE/keys/:idowner or admin
OperationWhat happensWhat survives
RotateA new secret is issued and shown once with a fresh upstream cap, and the old secret's upstream spend is cut off.The key id, the name, the cap, the settings and the entire usage history. Only the secret changes.
RevokeThe key is disabled and its upstream spend is cut off with it. Requests using it return 401 immediately.The usage rows. Revoking never deletes your log.
rotate and revoke
POST https://docketrouter.ai/api/v1/keys/672f0e35-…/rotate
{ "id": "672f0e35-…", "name": "prod-web", "key": "dr-…", "limit_usd": 10,
  "rotated_at": "2026-08-27T18:40:02.113Z" }

DELETE https://docketrouter.ai/api/v1/keys/672f0e35-…
{ "ok": true, "id": "672f0e35-…", "disabled": true }
  • Rotating an already-disabled key returns 400. Revoke is terminal; there is no un-revoke.
  • Rotation is the safe move for a suspected leak: it is a single call, it keeps the id you have wired into your own systems, and it kills the old secret's ability to spend at the same moment.
  • Because rotation preserves the key id, files owned by a keyless key stay reachable after rotation. Revoking that key does not delete its files.

Deploying a rotation without downtime

Rotation invalidates the old secret immediately, so there is no overlap window on a single key. To rotate with zero failed requests, mint a second key first, deploy it, then revoke the first. That also gives you a clean cutover in the usage log, because the two keys have different key_name values on every row.

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.