DocumentationBrowse
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
| Endpoint | API key | Signed-in session |
|---|---|---|
POST /chat/completions | yes | yes |
GET, POST /files | yes | yes |
DELETE /files/:id | yes | yes |
GET /usage, /usage/daily, /apps | yes (its own rows) | yes (all keys on the account) |
GET /usage/:request_id, /generation | yes | yes |
POST /usage/sync | yes | yes |
GET /auth/key | yes | yes |
GET /credits | yes (the account behind the key) | yes |
GET /billing/balance, /billing/autotopup | no (401) | yes |
GET, POST /keys | no (401) | yes |
GET, PATCH, DELETE /keys/:id | no (403) | yes, if you own the key |
POST /keys/:id/rotate | no (403) | yes, if you own the key |
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.
{
"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_spentandlimit_remainingare DocketRouter's own counters, the ones behind the 402 on a capped period. Withlimit_reset: "none",limit_remainingislimitminusspent; with a schedule it islimitminusperiod_spent. See limit_reset.- A due reset is rolled before the response is built, so
period_spentis never stale. rate_limitis the token bucket for this key: sustained requests perintervaland theburstit can absorb. The same numbers come back on every chat response asX-RateLimit-*headers.usage,usage_dailyandusage_monthlyare the upstream provider's figures when the host has management access, elseusagefalls back tospentand the other two arenull.data_policythroughrequire_parametersis the full provider and data policy on the key, decoded, the same fieldsGET /keys/:idreturns.localis DocketRouter's own tally for this key, includingfakes_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.
| Setting | Values | Effect | Who can set it |
|---|---|---|---|
name | 1 to 64 characters | Label only. Appears on every usage row as key_name. | Owner or admin |
limit_usd | number | The 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_reset | none, daily, weekly, monthly | How often the cap in limit_usd starts over. Changing it starts a fresh period at once. Details below. | Owner or admin |
jurisdiction | tx, ca, ny, fed | The default jurisdiction for every request on this key. A request can still override it per call. | Owner or admin |
log_content | boolean | Off 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 |
routing | shared, no-train, zdr, pinned | Provider routing policy, applied on top of any request-level preferences. | Admin |
pin_provider | provider slug | The provider pinned when routing is pinned. | Admin |
data_policy | shared, private_pod | A 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. |
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.
{
"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
{ "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
keyis the only copy. There is no endpoint that returns it again. limit_resetdefaults tonone. 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.
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.
| Value | Period starts | Period ends | The cap applies to |
|---|---|---|---|
none | never | never | spent_usd |
daily | 00:00 UTC today | 00:00 UTC tomorrow | period_spent_usd |
weekly | 00:00 UTC on Monday | 00:00 UTC next Monday | period_spent_usd |
monthly | 00:00 UTC on the 1st | 00:00 UTC on the 1st of next month | period_spent_usd |
The counters
| Field | Meaning |
|---|---|
spent_usd | Lifetime spend on this key. Never resets. Survives a schedule change, a rotation, and a revoke. |
period_spent_usd | Spend since limit_reset_at. Zeroes when the period rolls. Equal to spent_usd when limit_reset is none. |
remaining_usd | limit_usd minus the counter the cap applies to, floored at zero. null when limit_usd is null. |
limit_reset_at | UTC start of the current period. null with no schedule. |
resets_at | UTC 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_usdgoes to zero andlimit_reset_atmoves 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_resetto any value, including the one it already has, starts a fresh period immediately:period_spent_usdzeroes andlimit_reset_atre-anchors to the current period's start. - Never resets:
spent_usd, the usage rows, the key'slocaltotals, and the upstream lifetime cap behind the key. Switching a key back tononemakes the cap apply to lifetime spend again, so a key that has already spent more thanlimit_usdin 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.
{
"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, raiselimit_usd, or move the workload to another key. - A key with
limit_reset: nonenever 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.
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/checkoutreturns a Stripe-hosted Checkout URL. Send exactly one ofamount_cents, a preset (1000, 2500 or 10000, the $10 / $25 / $100 buttons), oramount_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 theurland theamount_centsthe session will charge. - Auto top-up.
GET /billing/autotopupreads the settings andPUT /billing/autotopupchanges them:enabled,threshold_usd(charge when available API credits fall below this; default 5, allowed 1 to 500) andamount_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 withenabled: falseand returns asetup_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: truereturns a freshsetup_urlwithout touchingenabled. One charge is made per crossing of the threshold, never more thanamount_usd, never while a previous charge is still pending, and three consecutive failed charges switch it off (disabled_reasonsays why; turning it back on resets the count). The response also reportscard_on_file,brandandlast4,pending, andavailable_cents, the figure compared against the threshold. - Monero. When enabled on the host,
POST /billing/moneroissues 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.
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 afterwardsWhile 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
| Operation | What happens | What survives |
|---|---|---|
| Rotate | A 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. |
| Revoke | The 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. |
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.