DocumentationBrowse
Production checklist
Everything between a working integration and one you would put in front of a firm. None of this is exotic; all of it is the thing that goes wrong at 4pm on a Friday.
Work down the list. Each item states what to do and why it matters here specifically.
Keys
- One key per environment, minimum. Separate keys give you separate usage rows, separate caps and separate blast radius.
key_namelands on every row, so this is also how you tell staging traffic from production in the log. - Set the cap to what a bad day costs, not to what a good day costs. Each key's cap is enforced upstream, so the cap is a real ceiling rather than an alert. A runaway loop can cost at most that number. Self-serve accounts are limited to $10 per key and 3 active keys during beta.
- Never ship a key to a browser or a mobile binary. A key can read your entire usage log, including logged content if you enabled it, and can upload and delete case files. It cannot mint keys or raise its own cap, which is the one thing it deliberately cannot do, and that is not enough on its own.
- Rotate by minting, not by rotating, when uptime matters.
POST /keys/:id/rotateinvalidates the old secret immediately, so there is no overlap window. To rotate with zero failed requests, mint a second key, deploy it, confirm traffic has moved in the usage log, then revoke the first. - Have the leak drill written down before you need it. Rotate or revoke, then read
GET /keys/:idfor the upstreamusageandlimit_remainingto see what was actually spent, andGET /usagefiltered by that key to see what was actually asked. - Verify with a call that costs nothing. GET /auth/key proves the credential works and reports the remaining cap without touching a model. It is the right post-deploy check.
Data policy: what is stored and what is not
Answer this before a firm asks, because they will.
| Data | Stored? | Where and for how long |
|---|---|---|
| Your prompt and the answer | No, by default | Only when log_content is switched on for the key. Then the last 6 messages and the answer are written to the usage row and returned by GET /usage/:request_id. The list endpoint never returns them; it returns has_content instead. |
| Citation strings from your prompt and answer | Yes | The verification report on each usage row contains the citation strings that were checked, for example 556 U.S. 662. That is content-derived data and it is retained even when content logging is off. It is citations only, never surrounding text. |
| Request metadata | Yes | Model, provider, token counts, cost, latency, retrieval time, source counts, jurisdiction, status and any upstream error message. This is the billing and audit record. |
| Case file text | Yes | That is the point of uploading it. Stored per owner, chunked, retrievable only by that owner and only when a request sets case_file. No automatic expiry: it lives until you delete it. |
| Anything shared across customers | No | Case files are never mixed into the public legal index and are never used to answer anyone else's question. |
| Site page views (docketrouter.ai itself, not the API) | Yes, 90 days | Path, referrer domain, browser/OS family, country when available, a salted one-way IP hash, and your account id when signed in. No raw IP, no cookies, no third-party script. See the policy page's What we log. |
- Decide on content logging deliberately. It is off by default. On, it is the difference between being able to debug a complaint about an answer and not. Off, it is the difference between storing privileged material and not. Both are defensible; drifting into one by accident is not.
- Delete case files when the matter closes. There is no retention timer, so this has to be something your system does.
GET /filesplusDELETE /files/:idis the whole API. - Choose a routing policy if the model provider matters to you. A key can be set to
no-trainorzdr, which force data-collection denial upstream, or to a pinned provider. For work that cannot leave your infrastructure at all, a private-pod key restricts the request to in-houselocal/models. These are key settings, so a request cannot downgrade them. - Know where verification runs. Answer-side citation checking runs against DocketRouter's own library. On a private-pod key, any check that would reach outside the pod is switched off unless the deployment has explicitly opted in.
Rate-limit headroom
A key gets 120 requests per minute sustained with a burst of 40, enforced by a token bucket that refills at 2 per second. Grounded requests take seconds each, so the practical ceiling is usually your own concurrency rather than the limit.
- Size concurrency for the sustained rate, not the burst. Eight in flight is a comfortable batch setting. Forty in flight will drain the bucket in one second and then crawl.
- Honour
retry-afterexactly. It is the number of seconds until a token is available, rounded up. Retrying sooner is guaranteed to fail again and costs you another 429. - Do not retry 400s. The allowlist check happens after retrieval and after the rate-limit check, so a bad model name burns a token and pays retrieval latency each time. Validate against
callable_modelsfrom GET /models in your own client. - Add a client-side queue rather than relying on retries. Headroom is not measurable from outside. A bounded queue in your own code is the only thing you control.
- Remember the timeouts. 90 seconds non-streaming, 120 seconds streaming. Set your own client timeout above those, not below, or you will abandon requests you are still billed for.
Observability
Log these four things on every call
| Field | From | Why |
|---|---|---|
request_id | The x-docketrouter-request-id header, available before you parse the body | The only handle support can use, and the key into GET /usage/:request_id. Log it on failures too. |
degraded | docketrouter.degraded | A rising share of degraded responses is an incident, and it is invisible unless you count it. |
verification.fabricated | docketrouter.verification | The number that justifies the product. Track it as a rate, per model and per question type. |
usage.cost | usage.cost | Reconcile it against your own tally weekly. A drift means something is retrying that should not be. |
Alerts worth having
- Degraded rate. In the usage log,
juiced = 1withsources_n = 0is a degraded request after the fact. Alert on the share over a rolling hour, not on single events. - Fabrication rate.
citations_not_foundper request, andfakes_caughtin the totals. - p95 latency.
latency_msandretrieval_msare separate columns, so you can tell a slow model from a slow index. - Cost per day per key.
GET /usagetotals, filtered by date. - Index health.
fused_indexinGET /api/healthpredictsdegraded.index_unreachable. The check does real work, so poll it at minutes, not seconds.
# Cost is an estimate at write time and truth after sync. Run this hourly.
curl -s -X POST "https://docketrouter.ai/api/v1/usage/sync?limit=200" \
-H "Authorization: Bearer $DOCKETROUTER_API_KEY"
# {"synced":12,"unresolved":0,"remaining":0}Interface obligations
The metadata is only worth generating if the interface acts on it.
- A fabricated citation blocks the normal presentation. Name the citations and require an explicit action to see the text. This is the one behaviour that has to be non-negotiable in your product, because it is the failure that ends up in a sanctions order.
- Unverified is not a fabrication. Render it as an amber caveat on the specific citation, with the
noteavailable. If your interface uses the same treatment for both, both signals become noise. - Degraded gets a persistent banner. It should still be on screen when the user copies the answer out. Use different copy for an index outage and for a genuine no-match; one invites a retry, the other invites a rephrase.
- A hostile case file goes in front of a person. Show the document name and the excerpt. Someone put text in that exhibit intended for your AI, and the lawyer on the matter needs to know.
- Never render a grounding badge from one signal. Grounded means
juicedis true,degradedis null, and at least one rule or case came back. - Say that verification is an existence check.
foundmeans the case is real. It says nothing about whether it supports the proposition. Lawyers respect the distinction and distrust products that blur it.
Failure drills
Run each of these once against a real key before you launch.
| Drill | How to force it | What should happen |
|---|---|---|
| Degraded retrieval | Ask a nonsense question with rules: false. See Degraded retrieval. | Your banner appears, no sources panel is drawn, and nothing claims the answer was grounded. |
| Rate limit | Fire 55 requests concurrently on one key. Roughly 40 succeed and the rest return 429 with retry-after: 1. | Your client sleeps for the header value and completes every request without a user-visible error. |
| Oversized request | Send a body above 512KB. | 413 invalid_request_error, and your client splits or rejects rather than retrying. |
| Rejected model | Send "model": "openai/gpt-5". | 400, and your client does not retry it. Check that your fallback logic reads callable_models rather than guessing. |
| Hostile case file | Upload a document containing an instruction override and ask about it with injection_policy: "block", then again with "flag". | 422 under block; under flag, the answer identifies the embedded instruction and your interface surfaces the finding. |
| Empty answer | Send max_tokens: 120 on a reasoning-heavy question. | 200 with empty content and finish_reason: "length", and your client raises rather than rendering nothing. |
| Revoked key | Revoke a key and replay a request with it. | 401 authentication_error, and your alerting distinguishes it from a 429. |
Degraded retrieval and the empty answer are the two failure modes most likely to reach a user unnoticed, because both arrive as HTTP 200. Put both in your test suite. Everything else announces itself with a status code.
When something goes wrong
- Find the request id. From your logs, the
x-docketrouter-request-idheader,docketrouter.request_id, or the text of a 502 message, which embeds it. - Pull the row.
GET /usage/:request_idreturns the provider that served it, exact token counts, retrieval time, the verification report, the upstream error message, and the content if the key had logging on. - Check whether it was grounded.
sources_nat 0 withjuicedat 1 means the answer was degraded, which explains a large share of "the answer was wrong" reports on its own. - Check the provider. Two identical requests can be served by different providers and different quantizations. If quality varies between them, pin one with
docketrouter.pinand compare. - Mail us with the request id. hello@docketrouter.ai. Without it, nobody can find anything.
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.