Overview
The CaseDiver API is a versioned REST surface at https://casediver.com/api/v1. It exposes three capabilities over a verified caselaw corpus: case search, cite checking (a single citation, a batch, or an entire uploaded document), and case retrieval (the opinion PDF with its processing material appended).
Every request and response is JSON, except where a PDF is the product. Every response — success or failure — carries a requestId. Quote it in support requests; it keys the exact ledger row and log line for your call.
# No key required — lists endpoints and current prices
curl https://casediver.com/api/v1Authentication
Send your API key on every request. Two header forms are accepted; the bearer form is preferred because it is what HTTP clients and API gateways already understand.
Authorization: Bearer lt_live_xxxxxxxxxxxxxxxxxxxx
# or
X-API-Key: lt_live_xxxxxxxxxxxxxxxxxxxxLudwig1011 is accepted on every endpoint. It resolves to a shared synthetic account and its usage is metered like anyone else's. It exists so integrations can be written before key issuance is self-service, and it will be removed before public launch. Do not build against it.Getting a key
Keys are issued by CaseDiver. A key is shown exactly once, at creation — only a SHA-256 hash is stored, so it cannot be recovered or re-displayed. If a key is lost, revoke it and issue a new one. Revocation takes effect on the next request.
Responses and errors
Successful responses carry the endpoint's payload plus two envelope fields: usage (what this call cost) and requestId.
{
"results": [ ... ],
"usage": {
"operation": "case_search",
"quantity": 7,
"costMillicents": 210,
"costCents": 21,
"breakdown": { "cases": 7 }
},
"requestId": "req_Ab3xK9pQ"
}Errors use the same envelope with an error object. Branch on error.code, not on the message — codes are stable, messages may be reworded.
{
"error": {
"code": "invalid_request",
"message": "Invalid search request.",
"details": [
{ "field": "jurisdiction.state", "message": "jurisdiction.state is required when type is 'one_state'" }
]
},
"usage": null,
"requestId": "req_Ab3xK9pQ"
}| Field | Type | Description |
|---|---|---|
missing_api_key | 401 | No key was presented. |
invalid_api_key | 401 | The key is unknown, revoked, or expired. These are not distinguished on purpose. |
account_suspended | 403 | The account exists but is not permitted to make calls. |
rate_limited | 429 | Too many requests. Honour Retry-After. |
invalid_request | 400 | Validation failed. details names the offending fields. |
not_found | 404 | No such case, job, or report. |
unsupported_media_type | 415 | Document upload was not a PDF or Word file. |
payload_too_large | 413 | Uploaded document exceeds the size limit. |
service_unavailable | 503 | A dependency was briefly unavailable. Nothing was charged; retry. |
internal_error | 500 | Our fault. Quote the requestId. |
200 with status: "did_you_mean" and up to three candidates. The request was understood and usefully answered, so treating it as an error class would make your error handler swallow the suggestions.Billing model
You are charged for what you receive, never for what you attempted. That single rule explains every price below.
| Field | Type | Description |
|---|---|---|
Case search | per case | Charged per case actually returned. A search that matches nothing is free. |
Cite check (cite) | per citation | One unit per citation checked, including citations that do not resolve — "this is not in the corpus" is the answer you paid for. |
Cite check (document) | per citation + per page | Citations found plus pages processed, and only once a report PDF exists. A failed job is free. |
Case retrieval | per case | Charged per case delivered. A miss is free, and so is a did-you-mean list — only your choice of a candidate is billed. |
Current prices are in the discovery document (GET /api/v1) and, for your own account including any negotiated rate, in GET /api/v1/usage. Each call's usage block reports the exact charge applied, and the price in force at call time is recorded on the ledger row, so a later price change never rewrites a past charge.
quantity: 0. That row is the evidence the work was done and deliberately not billed, which is what an invoice question needs.Idempotency
Send an Idempotency-Key header on any billable call. If the same key is replayed for the same account, the stored response is returned with replayed: true and no second charge. Use it whenever a network timeout leaves you unsure whether a call landed — retrying with the same key is always safe.
curl -X POST https://casediver.com/api/v1/search \
-H "Authorization: Bearer $LAWTOOLS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-request-2026-08-13-001" \
-d '{"query":"adverse possession","jurisdiction":{"type":"one_state","state":"FL"},"limit":5}'Rate limits
Requests are limited per minute per account. Every response carries your budget, and a 429 tells you exactly how long to wait.
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
Retry-After: 12 # only on 429Your limit is reported in GET /api/v1/usage. If you need a higher ceiling, ask — it is a per-account setting, not a plan tier.
Quickstart
A minimal TypeScript client. It centralizes the key, surfaces the requestId on failure, and adds nothing else — the API is plain HTTP and does not need an SDK.
const BASE = 'https://casediver.com/api/v1';
const KEY = process.env.LAWTOOLS_API_KEY!; // server-side only
async function call<T>(path: string, body?: unknown, idempotencyKey?: string): Promise<T> {
const res = await fetch(BASE + path, {
method: body ? 'POST' : 'GET',
headers: {
Authorization: `Bearer ${KEY}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!res.ok) {
// Quote requestId in support tickets; it keys our log line and ledger row.
throw new Error(`${json.error?.code}: ${json.error?.message} (${json.requestId})`);
}
return json as T;
}
const found = await call<{ results: Array<{ caseName: string; citation: string }> }>('/search', {
query: 'promissory estoppel reliance damages',
jurisdiction: { type: 'one_state_plus_federal', state: 'NY' },
limit: 5,
});
for (const r of found.results) console.log(r.caseName, '—', r.citation);Case search
/api/v1/searchCharged per case returnedSearches the caselaw corpus and returns cases with the passage that made each one relevant. Jurisdiction is required — an unscoped national search is the slowest query available and is almost never what was intended.
Request body
| Field | Type | Description |
|---|---|---|
queryrequired | string | What to search for: a citation, a case name, or a description of the issue. |
jurisdictionrequired | object | Which courts to search. See the table below. |
searchType | enum | auto (default), citation, case_name, keyword, semantic, or hybrid. Leave it out unless you know what you have. |
limit | integer | Cases to return, 1–200, default 10. Because pricing is per case, this is your budget for the call. Capped by your account's maxCasesPerSearch. |
filters.dateFrom | string | YYYY-MM-DD. Only cases filed on or after this date. |
filters.dateTo | string | YYYY-MM-DD. Only cases filed on or before this date. |
filters.publishedOnly | boolean | Exclude unpublished dispositions. |
filters.goodLawOnly | boolean | Exclude cases with negative treatment. Off by default: bad law is flagged, not hidden, because you usually need to know it exists. |
searchType
Omitting searchType routes the query automatically and is the recommended default. Pinning an engine is worth it when you already know what the string is — it removes both the latency and the false positives of engines that had nothing to contribute.
| Field | Type | Description |
|---|---|---|
auto | default | The router inspects the query and picks. Use this unless you have a reason not to. |
citation | pinned | The string is a reporter citation. |
case_name | pinned | The string is a party name. |
keyword | pinned | Literal term matching, including boolean operators. |
semantic | pinned | Meaning-based retrieval; the query is a description of an issue. |
hybrid | pinned | Keyword and semantic together, fused. |
Jurisdiction
Eight choices. Some require a companion field, and the validation error will name it if you omit it.
| Field | Type | Description |
|---|---|---|
all_states | — | Every state court, no federal courts. |
all_states_and_federal | — | Everything. The broadest and slowest scope. |
all_federal | — | Every federal court, including the Supreme Court. |
one_state | + state | That state's own courts only. Requires state, a USPS code such as "FL". |
one_state_plus_federal | + state | The state's courts, its regional circuit, and the U.S. Supreme Court — what a practitioner in that state actually cites. Requires state. |
federal_circuit | + circuit | One circuit plus the Supreme Court. Requires circuit: "1"–"11", "dc", or "federal". |
federal_district | + districtState | The federal district courts sitting in a state. Requires districtState, e.g. "TX" for the districts of Texas. |
us_supreme_court | — | The U.S. Supreme Court alone. |
Example
curl -X POST https://casediver.com/api/v1/search \
-H "Authorization: Bearer $LAWTOOLS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "qualified immunity excessive force",
"jurisdiction": { "type": "federal_circuit", "circuit": "11" },
"limit": 5,
"filters": { "dateFrom": "2015-01-01" }
}'Response
{
"results": [
{
"caseId": "4218873",
"caseName": "Smith v. City of Miami",
"citation": "812 F.3d 1234",
"court": "United States Court of Appeals for the Eleventh Circuit",
"courtAbbreviation": "11th Cir.",
"jurisdiction": "Eleventh Circuit",
"dateFiled": "2016-02-11",
"year": 2016,
"published": true,
"citedByCount": 74,
"snippet": "...the officers' use of force was not objectively reasonable...",
"snippetSource": "citing_opinion",
"goodLaw": {
"status": "good_law",
"negative": false,
"unknown": false,
"negativeTreatmentCount": 0,
"basis": "No negative treatment among 74 citing opinions."
},
"matchExplanation": "Matched on keyword and semantic recall.",
"retrievalUrl": "https://casediver.com/api/v1/cases/4218873/pdf"
}
],
"total": 1,
"totalAvailable": 46,
"searchInfo": {
"enginesUsed": ["keyword", "semantic"],
"jurisdictionLabel": "the 11th Circuit and the U.S. Supreme Court",
"citationsDetected": [],
"liftedPhrases": [],
"bodyTextSearchUnavailable": false,
"latencyMs": 412
},
"suggestion": null,
"usage": { "operation": "case_search", "quantity": 1, "costCents": 3, "breakdown": { "cases": 1 } },
"requestId": "req_Ab3xK9pQ"
}| Field | Type | Description |
|---|---|---|
total vs totalAvailable | integer | total is what you were sent and billed for; totalAvailable is how many matched overall. Raise limit to see more. |
goodLaw.unknown | boolean | True means treatment has not been determined — not that the case is good. Never present it to a user as a clean bill of health. |
snippet | string | The passage that made the case relevant, often how a later court described it rather than the opinion’s own words. |
retrievalUrl | string | A ready-made link to the case PDF. Billable when fetched. |
searchInfo.bodyTextSearchUnavailable | boolean | True when keyword recall covered case names, syllabi, and holdings only because a full-text index was unavailable. Explains an unexpectedly thin result set. |
suggestion | string | null | Present only on an empty result set — a broader query worth trying instead. |
Cite check — a citation
/api/v1/citecheck/citeCharged per citation checkedChecks whether a citation resolves to a real case, returns the correct Bluebook form, and reports good-law status. Send citation for one, or citations for up to 50 in a single call.
Request body
| Field | Type | Description |
|---|---|---|
citation | string | One citation, as written. A case name works too. |
citations | string[] | Up to 50 citations. Billed as one ledger row with quantity = the number checked, so your invoice matches your call log. |
Supply exactly one of the two. Beyond 50 citations, use the document endpoint — it is asynchronous and paces itself rather than holding a connection open.
Verdicts
| Field | Type | Description |
|---|---|---|
valid | verdict | The citation keys to exactly one case. correctedCitation carries proper Bluebook form; one candidate is returned. |
likely_valid | verdict | Candidates were found by name or partial signal, but the citation as written keys to none of them — a transposed volume, wrong page, or misremembered reporter. Up to three candidates, and deliberately no pick. |
not_found | verdict | Nothing matched. This is not proof the citation is fabricated; see corpusCaveat. |
error | verdict | This one citation could not be checked. Reported explicitly rather than omitted, because an omitted row reads as "checked and fine". |
likely_valid no candidate is chosen for you. Quietly rewriting a citation to a case the author never read is the worst thing a cite checker can do, so the choice stays with you. Present the candidates; let a human pick.corpusCaveat is set and says so. Do not label a citation fake on this basis alone.Example
curl -X POST https://casediver.com/api/v1/citecheck/cite \
-H "Authorization: Bearer $LAWTOOLS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"citations":["570 U.S. 744","999 F.3d 1"]}'{
"results": [
{
"citationAsWritten": "570 U.S. 744",
"verdict": "valid",
"correctedCitation": "United States v. Windsor, 570 U.S. 744 (2013)",
"explanation": "This citation resolves to a case in the corpus.",
"corpusCaveat": null,
"reporterKeys": ["us/570/744"],
"candidates": [
{
"caseId": "2812209",
"caseName": "United States v. Windsor",
"bluebookCitation": "United States v. Windsor, 570 U.S. 744 (2013)",
"citation": "570 U.S. 744",
"parallelCitations": ["133 S. Ct. 2675", "186 L. Ed. 2d 808"],
"court": "Supreme Court of the United States",
"year": 2013,
"published": true,
"citedByCount": 1204,
"goodLaw": { "status": "good_law", "negative": false, "unknown": false },
"matchedBy": "reporter_key",
"confidence": 1,
"retrievalUrl": "https://casediver.com/api/v1/cases/2812209/pdf"
}
]
}
],
"usage": { "operation": "citecheck_cite", "quantity": 2, "costCents": 6 },
"requestId": "req_Ab3xK9pQ"
}| Field | Type | Description |
|---|---|---|
matchedBy | enum | How the candidate was found: reporter_key, case_name, volume_page_near, or docket_number. |
confidence | number | 0–1. Exactly 1 only for a reporter-key match. |
reporterKeys | string[] | The reporter citations parsed out of your input. Empty means the string was not a citation at all, which is itself useful feedback. |
explanation | string | Plain language, safe to show a user verbatim. |
Cite check — a document
/api/v1/citecheck/documentPer citation found + per page, only if a report is producedUpload a brief or memo as multipart/form-data with a file field. Every case citation in it is extracted and checked, and the result is a report PDF listing each citation with its verdict — with the first page of each case found appended as an exhibit.
PDF, .docx, and .doc are accepted, up to 40 MB. The call returns a job id immediately; a 60-page brief takes 30–120 seconds, which is longer than any load balancer will hold a connection.
1. Upload
curl -X POST https://casediver.com/api/v1/citecheck/document \
-H "Authorization: Bearer $LAWTOOLS_API_KEY" \
-F "[email protected]"{
"jobId": "9f2c1a44-8e7b-4d2a-9c15-6b0e3f7a1d88",
"status": "queued",
"fileName": "brief.pdf",
"fileSize": 1841204,
"statusUrl": "https://casediver.com/api/v1/citecheck/jobs/9f2c1a44-.../",
"reportUrl": "https://casediver.com/api/v1/citecheck/jobs/9f2c1a44-.../report",
"pollAfterSeconds": 5,
"requestId": "req_Ab3xK9pQ"
}200, not 202: the state lives in the status field so one code path handles queued, processing, and completed. Nothing is charged at upload.2. Poll
/api/v1/citecheck/jobs/:idFreePoll every 2–5 seconds until status is completed or failed. The status response also carries the full per-citation findings, so you can render your own UI instead of the PDF if you prefer.
{
"jobId": "9f2c1a44-8e7b-4d2a-9c15-6b0e3f7a1d88",
"status": "completed",
"fileName": "brief.pdf",
"pageCount": 34,
"citationCount": 41,
"counts": { "valid": 36, "likelyValid": 3, "notFound": 2 },
"otherAuthoritiesFound": 7,
"citations": [ /* one entry per citation, same shape as the cite endpoint */ ],
"reportUrl": "https://casediver.com/api/v1/citecheck/jobs/9f2c1a44-.../report",
"error": null,
"createdAt": "2026-08-13T14:02:11.000Z",
"completedAt": "2026-08-13T14:03:04.000Z"
}| Field | Type | Description |
|---|---|---|
status | enum | queued, processing, completed, or failed. |
otherAuthoritiesFound | integer | Statutes, rules, and regulations detected but not verified — this product checks cases. Reported so their absence from the report is explained rather than mysterious. |
error | string | null | Why the job failed. A failed job is never charged. |
3. Download the report
/api/v1/citecheck/jobs/:id/reportFree — the charge was applied when the report was producedReturns application/pdf: a cover page with the tally, one entry per citation with its verdict and correct Bluebook form, and the first page of each case found appended behind it. Fetch it as many times as you like — you are billed for producing the report, not for downloading it.
not_found, because a cite-checked brief is privileged work product.Case retrieval
/api/v1/cases/retrieveCharged only when a case is deliveredResolves a citation or a case name to one case. Three outcomes, all of them 200: an unambiguous hit (charged), a did-you-mean list (free), or nothing found (free).
Request body
| Field | Type | Description |
|---|---|---|
queryrequired | string | A citation (410 U.S. 113) or a case name (Roe v. Wade). Both go to the same resolver. |
caseId | string | Your answer to a previous did-you-mean. Supplying it skips resolution and delivers that case. |
Outcomes
| Field | Type | Description |
|---|---|---|
ok | charged | Exactly one case matched unambiguously. case holds it, case.pdfUrl links to the PDF. |
did_you_mean | free | Up to three candidates. Also returned when a citation is valid but two parallel reporters both resolved — delivering the wrong one of two cases is worse than asking. |
not_found | free | Nothing matched. corpusCaveat may explain that the case could exist but not yet be loaded. |
The did-you-mean round trip
curl -X POST https://casediver.com/api/v1/cases/retrieve \
-H "Authorization: Bearer $LAWTOOLS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"Smith v. Jones"}'{
"status": "did_you_mean",
"message": "More than one case could match... Choose one and call this endpoint again with its `caseId` (no charge for this response).",
"candidates": [
{
"caseId": "1183422",
"caseName": "Smith v. Jones",
"bluebookCitation": "Smith v. Jones, 421 So. 2d 1024 (Fla. 1982)",
"year": 1982,
"confidence": 0.82,
"matchedBy": "case_name",
"pdfUrl": "https://casediver.com/api/v1/cases/1183422/pdf"
}
],
"charged": false,
"usage": { "operation": "case_retrieval", "quantity": 0, "costCents": 0 },
"requestId": "req_Ab3xK9pQ"
}curl -X POST https://casediver.com/api/v1/cases/retrieve \
-H "Authorization: Bearer $LAWTOOLS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"Smith v. Jones","caseId":"1183422"}'The second call returns status: "ok" and is charged. Answering stays on the same endpoint so the two-step conversation does not force you to switch routes mid-flow.
Case PDF
/api/v1/cases/:id/pdfCharged per case delivered; a 404 is freeReturns the opinion as a PDF with its processing material appended: metadata, the citation with parallel cites, good-law status with the basis for it, and the extracted analysis. This is the URL handed out as retrievalUrl and pdfUrl elsewhere in the API, so those links work directly in a browser.
curl -L https://casediver.com/api/v1/cases/2812209/pdf \
-H "Authorization: Bearer $LAWTOOLS_API_KEY" \
-o windsor.pdfBilling headers
A PDF body cannot carry the JSON usage envelope, so the charge travels in headers. Read them if you reconcile spend per download.
Content-Type: application/pdf
Content-Disposition: inline; filename="United States v. Windsor.pdf"
X-Request-Id: req_Ab3xK9pQ
X-CaseDiver-Operation: case_retrieval
X-CaseDiver-Charge-Units: 1
X-CaseDiver-Charge-Cents: 5Usage and pricing
/api/v1/usageFreeYour spend by operation, plus the prices and limits in force for your key — including any negotiated rate, which the public discovery document does not show.
curl "https://casediver.com/api/v1/usage?days=30" \
-H "Authorization: Bearer $LAWTOOLS_API_KEY"{
"consumer": { "name": "Acme Legal", "email": "[email protected]", "status": "active" },
"periodDays": 30,
"since": "2026-07-14T00:00:00.000Z",
"byOperation": [
{ "operation": "case_search", "calls": 412, "units": 3180, "costCents": 954 },
{ "operation": "case_retrieval", "calls": 96, "units": 88, "costCents": 440 }
],
"totalCostCents": 1394,
"yourPricing": {
"caseSearchPerCase": 3,
"citeCheckPerCitation": 2,
"citeCheckPerPage": 1,
"caseRetrievalPerCase": 5,
"rateLimitPerMinute": 60,
"maxCasesPerSearch": 100
},
"requestId": "req_Ab3xK9pQ"
}| Field | Type | Description |
|---|---|---|
days | query | Look-back window, 1–365, default 30. |
calls vs units | integer | Calls is requests made; units is what you were billed for. They differ whenever a call returns several cases or none. |
yourPricing | object | What your key is actually charged, which may be below the public rate. All values in cents. |
This is also the fastest way to tell a connectivity problem from a credentials problem: if GET /api/v1 answers but GET /api/v1/usage returns invalid_api_key, the network is fine and the key is not.
Recipes
Cite check a document, end to end
Upload, poll, download. Note the polling ceiling: a job that never finishes must fail your code rather than loop forever.
import { readFile } from 'node:fs/promises';
// 1. Upload
const form = new FormData();
form.append('file', new Blob([await readFile('brief.pdf')], { type: 'application/pdf' }), 'brief.pdf');
const started = await fetch(BASE + '/citecheck/document', {
method: 'POST',
headers: { Authorization: `Bearer ${KEY}` }, // no Content-Type: fetch sets the boundary
body: form,
}).then((r) => r.json());
// 2. Poll. 5s matches pollAfterSeconds; the cap turns a stuck job into an error.
let job = started;
for (let i = 0; i < 120 && (job.status === 'queued' || job.status === 'processing'); i++) {
await new Promise((r) => setTimeout(r, 5000));
job = await call(`/citecheck/jobs/${started.jobId}`);
}
if (job.status !== 'completed') throw new Error(`Cite check ${job.status}: ${job.error ?? 'timed out'}`);
console.log(`${job.citationCount} citations over ${job.pageCount} pages`, job.counts);
// 3. Download the report (free — the charge landed when it was produced)
const pdf = await fetch(`${BASE}/citecheck/jobs/${started.jobId}/report`, {
headers: { Authorization: `Bearer ${KEY}` },
}).then((r) => r.arrayBuffer());Retrieve a case, handling did-you-mean
Resolution can need two calls. The first is free when it is ambiguous, so the branch below costs nothing until a case is actually chosen.
let hit = await call<any>('/cases/retrieve', { query: 'Smith v. Jones' });
if (hit.status === 'did_you_mean') {
// Present hit.candidates to a human. Auto-picking the top candidate is a
// product decision — it can silently return a case nobody asked for.
const chosen = hit.candidates[0];
hit = await call<any>('/cases/retrieve', { query: 'Smith v. Jones', caseId: chosen.caseId });
}
if (hit.status === 'not_found') {
console.log('No match.', hit.corpusCaveat ?? '');
} else {
const pdf = await fetch(hit.case.pdfUrl, {
headers: { Authorization: `Bearer ${KEY}` },
}).then((r) => r.arrayBuffer());
}Retry safely after a timeout
When a call times out you cannot tell whether it landed. An idempotency key makes the retry free and returns the original answer.
const key = `search-${crypto.randomUUID()}`; // one key per distinct request
async function withRetry<T>(path: string, body: unknown): Promise<T> {
for (let attempt = 0; attempt < 3; attempt++) {
try {
return await call<T>(path, body, key); // same key: at most one charge
} catch (err) {
if (attempt === 2) throw err;
await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
}
}
throw new Error('unreachable');
}Python
import os, requests
BASE = "https://casediver.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['LAWTOOLS_API_KEY']}"
res = session.post(f"{BASE}/citecheck/cite", json={"citations": ["570 U.S. 744", "999 F.3d 1"]})
res.raise_for_status()
payload = res.json()
for item in payload["results"]:
print(item["citationAsWritten"], "->", item["verdict"])
if item["verdict"] == "likely_valid":
for cand in item["candidates"]:
print(" did you mean:", cand["bluebookCitation"])
print("charged (cents):", payload["usage"]["costCents"])The local tester
A standalone app in tools/api-tester exercises every endpoint with a form per route, shows latency, rate-limit headroom, and the exact charge for each call, and previews returned PDFs inline. It is the fastest way to see a response shape before writing code against it.
cd tools/api-tester
npm install
npm run dev # http://localhost:5199
# Point it at a different server (defaults to http://localhost:4000)
$env:LAWTOOLS_API_URL = "https://casediver.com"; npm run devRequests are proxied through the dev server, so the browser treats them as same-origin and PDFs render inline without CORS configuration. Your key is kept in localStorage and sent only to the proxied API.
Support
Include the requestId from the failing response in any support request. It resolves to the exact log line and ledger row, which turns most questions into a one-look answer. For a higher rate limit, an additional key, or a volume rate, ask — all three are per-account settings rather than plan tiers.