docketrouter
DocumentationBrowse
API reference

Chat completions

OpenAI-compatible request and response, plus one request field and one response field of our own. Everything the grounding layer does is driven by the docketrouter object on the request and reported in the docketrouter object on the response.

POST/chat/completionskey, session or site demo

Request parameters

FieldTypeDefaultNotes
modelstringrequiredMust be on the callable allowlist. Today that is deepseek/deepseek-v4-flash. A key with a private-pod data policy uses local/… ids instead.
messagesarrayrequiredAt least one. Each item is { role, content } where role is system, user or assistant and content is a plain string. Array content parts and the tool and function roles are rejected by the schema.
streambooleanfalseServer-sent events. See Streaming.
temperaturenumberprovider defaultPassed through unchanged. The revise pass always uses 0 regardless.
max_tokensnumberprovider defaultOutput token ceiling. Values above 8,192 are rejected with 400. Use 1,000 or more: reasoning tokens come out of this budget.
modelsstring[]noneUp to 5 fallback models. Every entry is checked against the same allowlist as model, so a fallback cannot reach a model that is off.
providerobjectnoneUpstream provider preferences, passed through: order, only, ignore, quantizations, sort, max_price, data_collection, zdr. Key-level routing policy is applied on top and wins.
response_formatobjectnoneStructured output, for example a json_schema block. Setting it also requires parameter support upstream, so the request is only routed to endpoints that can honour it.
docketrouterobjectall defaultsThe grounding layer controls. See below.
Unrecognised fields are dropped, not rejected

n, tools, tool_choice, stop, top_p, seed, logprobs, frequency_penalty and their relatives are not forwarded today, even when the underlying model supports them. Sending them is not an error and has no effect, which is the failure mode most likely to waste your afternoon.

The docketrouter options block

Every field defaults independently, so you can send only the one you care about.

FieldTypeDefaultWhat it does
juicebooleantrueMaster switch. When false nothing is retrieved, no citations are checked, your messages go to the model untouched, and both sources and verification come back null.
rulesbooleantrueRetrieve up to 6 rule and statute excerpts and inject them verbatim.
casesbooleantrueRetrieve up to 5 related opinions from the fused index, with fallback sources when the index returns nothing.
verifybooleantrueCheck reporter citations found in your prompt, and again in the answer.
revisebooleantrueWhen verification finds a citation that affirmatively does not exist, spend one extra model call rewriting the answer without it. Non-streaming only.
case_filebooleanfalseRetrieve up to 6 chunks from your own uploaded documents and quarantine them into the prompt. Requires juice: true.
injection_policy"flag" | "block""flag"What to do when a retrieved case-file document scores hostile. flag proceeds and reports; block refuses with 422.
jurisdictiontx | ca | ny | fedthe key setting, else txSelects which shards of the fused case index are searched, and is echoed and logged. See Grounding for what each value actually reaches.
session_idstringnoneUp to 128 characters, echoed back in the response metadata. Useful for correlating a thread of requests in your own logs.
pinstringnonePin one upstream provider endpoint by slug (for example deepinfra/fp8) and disable fallbacks, so answers come from a single provider and quantization.

Omitting the block entirely gives you the full default behaviour. Sending { "case_file": true } leaves every other field at its default, because the defaults are per field rather than per object.

The response

A standard chat.completion object with two additions: usage.cost (billed US dollars for this request) and the docketrouter block. The request id is also returned in the x-docketrouter-request-id response header.

200 OK
{
  "id": "chatcmpl-fee163e8-b48",
  "object": "chat.completion",
  "created": 1787848438,
  "model": "deepseek/deepseek-v4-flash",
  "choices": [
    { "index": 0,
      "message": { "role": "assistant", "content": "Under the Federal Rules of Civil Procedure, …" },
      "finish_reason": "stop" }
  ],
  "usage": {
    "prompt_tokens": 2921,            // includes the injected authorities
    "completion_tokens": 271,
    "total_tokens": 3192,
    "cost": 0.00030483                // billed USD, upstream price x 1.25
  },
  "docketrouter": {
    "sources": {                      // null when juice is false
      "rules": [ … ],                 // excerpts injected verbatim
      "cases": [ … ],                 // opinions offered to the model as leads
      "citations": [                  // citations found in YOUR prompt, each checked
        { "input": "556 U.S. 662", "status": "found", "note": "in DocketRouter index" }
      ],
      "ms": 2108                      // retrieval wall clock
    },
    "injection": null,                // case-file screening report; null unless case_file was used
    "degraded": null,                 // set when grounding was asked for and came back short
    "upstream": "openrouter",
    "juiced": true,
    "data_policy": "shared",
    "jurisdiction": "tx",
    "session_id": null,
    "verification": {                 // citations found in the ANSWER, each checked
      "checked": [ { "input": "556 U.S. 662", "status": "found", "note": "in DocketRouter index" } ],
      "fabricated": [],               // status "not_found": treat as a hard stop
      "unverified": [],               // status "unverified" or "ambiguous": no signal either way
      "revised": false                // whether the revise pass rewrote this answer
    },
    "request_id": "req_ad378ebfe63d41778571"
  }
}

Field reference

FieldTypeMeaning
usage.costnumberBilled US dollars for this request, including the revise call when one ran. Upstream price times 1.25.
docketrouter.sourcesobject | nullWhat was retrieved: rules, cases, citations, ms. Null when juice is false.
docketrouter.injectionobject | nullScreening report for the case-file chunks that were retrieved. Null when none were.
docketrouter.degradedobject | nullSet when grounding was requested and produced nothing, or when the primary index was unreachable. See Degraded retrieval.
docketrouter.upstreamstringopenrouter normally, private_pod for an in-house pod key.
docketrouter.juicedbooleanWhether the grounding layer ran at all.
docketrouter.data_policystringshared or private_pod. A property of the key, not of the request.
docketrouter.jurisdictionstringThe resolved jurisdiction, after the request value, the key setting and the default.
docketrouter.session_idstring | nullEchoed back exactly as sent.
docketrouter.verificationobject | nullAnswer-side citation report. Null when juice or verify is false.
docketrouter.request_idstringThe req_… handle. Also in the x-docketrouter-request-id header, and the key into GET /usage/:request_id.
docketrouter.generation_idstring | nullThe upstream generation id for the exact model call behind this answer. Also in the x-docketrouter-generation-id header. Quote it when disputing a charge.
docketrouter.providerstring | nullThe upstream provider that served the call. Also in the x-docketrouter-provider header.
A 200 can still carry no answer

When reasoning tokens consume the whole max_tokens budget you get HTTP 200, an empty choices[0].message.content and finish_reason: "length". Check for it explicitly. A client that only branches on the status code will ship an empty answer to a user.

Provider routing

Your preferences pass through; key policy is applied on top and wins.

SettingWhere it comes fromEffect
providerThe requestPassed to the upstream router unchanged.
docketrouter.pinThe requestSets the provider order to that one slug and disables fallbacks.
routing: "no-train"The keyForces data_collection: "deny".
routing: "zdr"The keyForces data_collection: "deny" and zero data retention.
routing: "pinned"The keyPins the key's configured provider and disables fallbacks, unless the request already sent pin.
data_policy: "private_pod"The keyForces data_collection: "deny" and restricts the request to in-house models. See below.

The provider that actually served a request is not knowable at request time. It appears on the usage row as provider once reconciled, and two identical requests can be served by different providers.

Private pod keys

  • A key whose data policy is private_pod may only call in-house models under the local/ namespace. Anything else returns 403, and the prompt never leaves the pod.
  • If the host has no pod endpoint configured, the request returns 503.
  • Citation verification against anything outside the pod is switched off for pod keys unless the firm has explicitly opted in.

Examples

curl https://docketrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer dr-…" \
  -H "content-type: application/json" \
  -d '{
    "model": "deepseek/deepseek-v4-flash",
    "max_tokens": 1200,
    "messages": [
      { "role": "user",
        "content": "Summarize the pleading standard in Ashcroft v. Iqbal, 556 U.S. 662 (2009)." }
    ],
    "docketrouter": { "jurisdiction": "fed", "session_id": "matter-4471" }
  }' | jq '{cost: .usage.cost, verification: .docketrouter.verification}'

Structured output works the same way as upstream. Send a response_format with a json_schema and the request is routed only to endpoints that support it. Grounding still runs, and the verification report still comes back beside your parsed object.

Validation order

Worth knowing, because it determines what a bad request costs you.

  1. authentication → 401
  2. body size, 512KB → 413
  3. JSON parse and schema → 400
  4. message count and total characters → 413
  5. rate limit → 429
  6. private-pod policy → 403 or 503
  7. grounding runs
  8. model allowlist and max_tokens ceiling → 400
  9. injection policy → 422
  10. model call → 200 or 502
Steps 7 and 8 are in that order on purpose, and it has a cost

A request naming a model that is not callable still pays for retrieval before it returns 400, and it still consumes a rate-limit token. If you route across models dynamically, check callable_models from GET /models in your own client rather than discovering the allowlist through 400s.

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.