Public API
Integrate citation resolution, citation verification, and legal Q&A into your applications.
Copies the full guide as Markdown for Claude, ChatGPT, Cursor, or similar — uses $OC_API_KEY, not your live key.
01Authentication
All requests require a bearer token. Include your API key in the Authorization header on every call.
Keep your API key out of client-side or browser code. Always store it in server-side secure storage and rotate keys regularly.
02Request / response contract
Request tracing
Pass a unique X-Request-Id header on every request. The same ID is echoed back as requestId in the response.
Success
{
"data": { /* endpoint-specific payload */ },
"requestId": "req_abc123"
}Error
{
"error": {
"code": "payment_required",
"message": "Insufficient balance",
"requestId": "req_abc123"
}
}All JSON field names use camelCase.
03Billing & wallet
Billing is tracked against a prepaid USD wallet. Each billable endpoint maps to a SKU.
citations_resolve
$15
per 1,000 units
citations_verify
$15
per 1,000 units
answers
$125
per 1,000 units
case_brief
$10
per 1,000 units
documents_review
$100
per 1,000 reviews
The case law library reads are unbilled, apart from the AI case brief. They share the `citations/resolve` rate-limit bucket rather than having their own.
Enforcement
- If wallet balance is <= 0 before work begins, the request fails immediately with 402 payment_required
- If balance is positive but lower than the computed charge, the request may still complete — the full charge then applies
- Rate-limited requests (429) are never billed
04Rate limits
When throttled, the API returns HTTP 429 with a Retry-After header indicating how many seconds to wait.
Limit total retry attempts and alert on sustained throttling — it signals you need to review throughput or upgrade your plan.
05Health
curl -sS "https://public-api.opencase.com/v1/health"Returns 200 when the API and its dependencies are healthy.
curl -sS "https://public-api.opencase.com/v1/health/ready"06Resolve citations
Body: `{ citations: string[], searchMode: string }`
curl -sS -X POST "https://public-api.opencase.com/v1/citations/resolve" \
-H "Authorization: Bearer $OC_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-Id: resolve-001" \
-d '{
"citations": [
"Brown v. Board of Education, 347 U.S. 483 (1954)"
],
"searchMode": "keyword"
}'Each result carries `caseStatus.caseId`. Pass it to any `/cases/{caseId}` read below.
07Verify citations
Body: exactly one of `{ text: string }` or `{ url: string }`, plus `country`. Text of 100,000 characters or fewer is verified inline and returns `200`; a `url`, or longer text up to the async ceiling, is queued and returns `202` with a `jobId`. Text past that ceiling returns `413 document_too_large` naming the limit that rejected it.
curl -sS -X POST "https://public-api.opencase.com/v1/citations/verify" \
-H "Authorization: Bearer $OC_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-Id: verify-001" \
-d '{
"text": "In Brown v. Board of Education, 347 U.S. 483 (1954), ...",
"country": "US"
}'
# A url is always queued: we cannot know the document's size until we fetch it.
curl -sS -X POST "https://public-api.opencase.com/v1/citations/verify" \
-H "Authorization: Bearer $OC_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-Id: verify-002" \
-d '{
"url": "https://example.com/appellate-brief.pdf",
"country": "US"
}'
# -> 202 { "data": { "jobId": "...", "status": "queued" }, "requestId": "..." }Extraction reads US citation forms only — send a non-US `country` to `/citations/resolve` with the citation strings instead. Every response carries `extractedCount`, the number of citations found, so zero found is not mistaken for a clean document; one request verifies up to 50 and sets `truncated: true` beyond that. A `url` must point at a PDF or DOCX — a web page is not a document.
Returns `queued`, `running`, `completed` or `failed`. A completed job carries `result`, the same payload the inline response would have.
curl -sS "https://public-api.opencase.com/v1/jobs/$JOB_ID" \
-H "Authorization: Bearer $OC_API_KEY"Never billed. Poll every 2s for the first 30s, then every 10s — `Retry-After` carries the same advice. Polling has its own rate-limit bucket of 600 requests a minute, so waiting on a job does not spend the quota you need for resolve or verify.
08Generate answers
Body: `{ question: string, country?: string }`. Optional `states`, `circuits` and `subdivisions` arrays narrow the jurisdiction the answer is written for. Omitting `country` searches every supported country.
curl -sS -X POST "https://public-api.opencase.com/v1/answers" \
-H "Authorization: Bearer $OC_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-Id: answers-001" \
-d '{
"question": "What is the current federal standard for summary judgment?",
"country": "US",
"states": ["California"]
}'{
"data": {
"answer": "## Short answer\nThe movant must satisfy [[case: Winter v. NRDC, 555 U.S. 7 (2008)]] ...[1]",
"sources": [
{ "title": "Winter v. NRDC", "url": "https://...", "published": "2008-11-12" }
],
"citations": [
{ "citation": "Winter v. NRDC, 555 U.S. 7 (2008)", "type": "case", "status": "verified", "url": "https://..." }
],
"stats": { "total": 1, "verified": 1, "unverified": 0 }
},
"requestId": "req_abc123"
}09Find a case
Body: `{ citation }` or `{ caseId }`. `caseId` takes either shape — a `caseStatus.caseId` from resolve, or an `authority.legalContentId`. Unbilled.
curl -sS -X POST "https://public-api.opencase.com/v1/cases/lookup" \
-H "Authorization: Bearer $OC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"citation": "Brown v. Board of Education, 347 U.S. 483 (1954)"
}'Always 200. `status` is `found` (use `caseId`), `not_in_library` (the decision exists but is not indexed here — a `courtListenerUrl` is returned), or `unresolved`. POST rather than GET so your citations never appear in a request URL or a server access log.
10Case law library
Query: `limit`, `cursor`, `filter` (`all` | `negative`), `sort`.
curl -sS "https://public-api.opencase.com/v1/cases/$OC_CASE_ID/treatments?filter=negative" \
-H "Authorization: Bearer $OC_API_KEY"curl -sS "https://public-api.opencase.com/v1/cases/$OC_CASE_ID/headnotes" \
-H "Authorization: Bearer $OC_API_KEY"curl -sS "https://public-api.opencase.com/v1/cases/$OC_CASE_ID/headnotes/$OC_HEADNOTE_ID/citations" \
-H "Authorization: Bearer $OC_API_KEY"curl -sS "https://public-api.opencase.com/v1/cases/$OC_CASE_ID/direct-history" \
-H "Authorization: Bearer $OC_API_KEY"Each item carries `citedCaseFlag` — the reason to read this list.
curl -sS "https://public-api.opencase.com/v1/cases/$OC_CASE_ID/authorities" \
-H "Authorization: Bearer $OC_API_KEY"Grouped, with `caseCount` = distinct citing cases that used that description.
curl -sS "https://public-api.opencase.com/v1/cases/$OC_CASE_ID/summaries" \
-H "Authorization: Bearer $OC_API_KEY"curl -sS "https://public-api.opencase.com/v1/cases/$OC_CASE_ID/summaries/$OC_GROUP_ID/citations" \
-H "Authorization: Bearer $OC_API_KEY"Returns `{ summary, judges }`. Billed 1 unit per request on success. A case with no brief returns 404 rather than an empty one, so you are never charged for a blank result.
curl -sS "https://public-api.opencase.com/v1/cases/$OC_CASE_ID/brief" \
-H "Authorization: Bearer $OC_API_KEY"The only billed read in this group — the summary was written by a model, not extracted.
Returns `{ format, content }`. `format` is `html`, `markdown`, or `text` when the stored provenance is unrecorded.
curl -sS "https://public-api.opencase.com/v1/cases/$OC_CASE_ID/body" \
-H "Authorization: Bearer $OC_API_KEY"Requires opinion content to be enabled on your account; otherwise `403 content_not_enabled`. `html` is stored as CourtListener supplied it — sanitize before rendering. `text` means escape it, do not parse it as markup.
11Review a contract
Body: `{ text: string, playbookId?: string }`
curl -sS -X POST "https://public-api.opencase.com/v1/documents/review" \
-H "Authorization: Bearer $OC_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-Id: review-001" \
-d '{
"text": "MUTUAL NONDISCLOSURE AGREEMENT\n\nThis Agreement is entered into ...",
"playbookId": "nda-standard"
}'Without `playbookId` the contract type is detected and only the validated types are reviewed — anything else returns `reviewed: false` and is not billed, as is a review that fails. Max 500,000 characters (413 above that), and the request body is capped at 1 MiB, so text outside the Latin alphabet may reach that first. The first 200,000 characters are analysed; every response reports `analyzedChars` against `totalChars`.
Returns `{ id, name, contractType, checks, autoDetected }` per playbook.
curl -sS "https://public-api.opencase.com/v1/documents/playbooks" \
-H "Authorization: Bearer $OC_API_KEY"Not billed. `autoDetected: false` means detection will not select that playbook on its own — it has not been validated against production documents and runs only when you name it.
12Error reference
All errors follow the same shape. Match on error.code for programmatic handling.
| Status | error.code | Meaning | Action |
|---|---|---|---|
| 401 | unauthorized | Missing or invalid API key | Send a valid bearer token |
| 402 | payment_required | Wallet balance is zero or negative | Add funds, review billing settings |
| 413 | document_too_large | Document exceeds the maximum size a verify job will accept | Split the document and submit each part |
| 503 | async_unavailable | Oversized or url input was sent where the job queue is not enabled | Send text of 100,000 characters or fewer |
| 400 | invalid_document_url | The url is not public https, or points at a non-public address | Host the document at a publicly reachable https URL |
| 400 | invalid_request_id | X-Request-Id starts with `job:`, a prefix reserved for internal use | Send any other X-Request-Id |
| 400 | document_unreadable | The document was fetched but cannot be read — an unsupported format, too many pages, or a scan with no text layer | Send a text-bearing PDF or DOCX, or post the text directly |
| 503 | document_service_unavailable | The document service could not be reached while fetching a url | Retry; a queued job retries this on its own |
| 400 | unsupported_jurisdiction | Verify cannot extract citations for the requested country (US only) | Send the citation strings to /citations/resolve instead |
| 429 | rate_limited | Too many requests | Respect Retry-After, exponential backoff |
| 409 | request_id_reused | This X-Request-Id was already billed on this endpoint | Retry with a new X-Request-Id |
| 502 | review_failed | The model answered, but not with a usable review (POST /documents/review) | Retry the request |
| 504 | gateway_timeout | The model did not finish in time (POST /answers, POST /documents/review) | Retry once; shorten the question or the document |
| 400 | bad_request | Request failed schema validation, or the answer model refused the question | Check request body shape and fields |
13Operational best practices
- Use separate API keys per logical service boundary and per environment (dev, staging, prod)
- Rotate keys regularly — revoke any keys you no longer use
- Never expose keys in client-side or browser code; use secure server-side storage
- Monitor error volume grouped by error.code and HTTP status
- Alert on sustained 402 (insufficient balance) and 429 (throttling) activity
- Always capture and log requestId — include it in every support ticket and incident report