SDKs
The eCourtDate Chatbot API speaks the OpenAI wire format on its chat, models, and embeddings endpoints, so the official OpenAI SDKs work unchanged. Two settings differ from a stock OpenAI setup: the base URL and the key.
base_url: https://api.chatbots.ecourtdate.com/v1
api_key: your ecd_sk_... key (see Authentication)
Keep the key in an environment variable (the examples below read API_KEY)
and never in source control. Keys are issued and activated for your account
by eCourtDate; see Authentication.
What "OpenAI-compatible" guarantees
Four operations follow the OpenAI request and response shapes exactly, so an official SDK can send the request and parse the response without modification:
| Operation | SDK call (Python / Node) |
|---|---|
POST /v1/chat/completions | client.chat.completions.create(...) |
GET /v1/models | client.models.list() |
GET /v1/models/{modelId} | client.models.retrieve(...) |
POST /v1/embeddings | client.embeddings.create(...) |
Within that surface:
- Every response parses. Success bodies are the OpenAI objects
(
chat.completion,chat.completion.chunk,model,embedding, and thelistenvelope). Error bodies are the OpenAI envelope{"error": {"message", "type", "param", "code"}}, so the SDKs raise their usual exception classes (Error handling). - Unknown request fields are accepted and ignored. Parameters the API
does not implement (
seed,presence_penalty,frequency_penalty,logit_bias,logprobs,parallel_tool_calls,store,service_tier, and similar) are accepted and have no effect;nmust be1. See Chat completions for the supported set. modelnames a bot. The value is a bot slug or alias configured for your account, ordefaultfor the account default (omitting it does the same). Responses echo the canonical slug; an unknown name is404model_not_found. See Models.- Extensions are additive. The API adds fields the OpenAI schema does
not define; the SDKs keep them and expose them as extra attributes:
citationson assistant messages: the knowledge-base passages the answer was drawn from, an array (possibly empty) on a text answer from a bot that cites andnullon tool-call turns and for bots that do not (Citations). In a stream it arrives on the finish chunk (Streaming).input_typeon embeddings requests (search_documentorsearch_query), sent through the SDK'sextra_body(Embeddings).
- Tool call ids are opaque strings. Do not parse or generate them; echo
them back unchanged as
tool_call_idintoolmessages (Tools and structured output).
The remaining endpoints (conversations, knowledge base ingestion, crawling, documents, and jobs) are eCourtDate-specific and are not part of the OpenAI SDK surface. Call them with a plain HTTP client (below).
Python SDK
Install the official OpenAI Python SDK (version 1.0 or later; the examples were verified with 2.x):
pip install openai
Configure the client and send a chat completion:
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.chatbots.ecourtdate.com/v1",
api_key=os.environ["API_KEY"],
)
completion = client.chat.completions.create(
model="default",
messages=[
{"role": "user", "content": "When is the hearing for case 2026-CV-1042?"}
],
)
print(completion.model) # canonical bot slug, e.g. "court-assistant"
print(completion.choices[0].message.content)
print(completion._request_id) # X-Request-ID, quote it to support
The SDK also honors the OPENAI_BASE_URL and OPENAI_API_KEY environment
variables, so OpenAI() with no arguments works when both are set.
Reading citations
citations is not part of the OpenAI ChatCompletionMessage model, so the
SDK stores it in the message's model_extra mapping (verified with
openai-python 2.46). The key is always present; its value is null on a
tool-call turn and for bots that do not cite, which the or [] below
absorbs:
message = completion.choices[0].message
citations = message.model_extra.get("citations") or []
for c in citations:
print(f"[Source {c['source_index']}] {c['source_filename']} "
f"(chunk {c['chunk_index']}, score {c['score']:.2f})")
to_dict() also includes extra fields, so message.to_dict().get("citations")
is equivalent. Each citation object has source_index, document_id,
source_filename, chunk_index, text_preview, and score; see
Citations.
Response headers
Use with_raw_response when you need headers such as RateLimit-Remaining:
raw = client.chat.completions.with_raw_response.create(
model="default",
messages=[{"role": "user", "content": "Where do I pay a filing fee?"}],
)
print(raw.headers["RateLimit-Remaining"], raw.headers["X-Request-ID"])
completion = raw.parse()
Node SDK
Install the official OpenAI Node SDK (version 4 or later; the examples were verified with 7.x):
npm install openai
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.chatbots.ecourtdate.com/v1",
apiKey: process.env.API_KEY,
});
const completion = await client.chat.completions.create({
model: "default",
messages: [
{ role: "user", content: "When is the hearing for case 2026-CV-1042?" },
],
});
console.log(completion.model); // canonical bot slug
console.log(completion.choices[0].message.content);
console.log(completion._request_id); // X-Request-ID
The Node SDK parses JSON into plain objects, so extension fields are present
on the parsed result. In JavaScript read citations directly; in TypeScript
the ChatCompletionMessage type does not declare it, so narrow the type:
import type { ChatCompletionMessage } from "openai/resources/chat/completions";
interface Citation {
source_index: number;
document_id: string;
source_filename: string;
chunk_index: number;
text_preview: string;
score: number;
}
const message = completion.choices[0].message as ChatCompletionMessage & {
citations: Citation[] | null;
};
for (const c of message.citations ?? []) {
console.log(`[Source ${c.source_index}] ${c.source_filename} (chunk ${c.chunk_index})`);
}
JSON.parse(JSON.stringify(completion)) is never required; the field is
already on the object. For response headers use .withResponse():
const { data, response } = await client.chat.completions
.create({ model: "default", messages: [{ role: "user", content: "Hi" }] })
.withResponse();
console.log(response.headers.get("ratelimit-remaining"));
curl
curl -s "https://api.chatbots.ecourtdate.com/v1/chat/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "default",
"messages": [
{"role": "user", "content": "When is the hearing for case 2026-CV-1042?"}
]
}'
{
"id": "chatcmpl-8a317b1a95a74321b73b6567",
"object": "chat.completion",
"created": 1787360527,
"model": "court-assistant",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The hearing for case 2026-CV-1042 is set for August 15, 2026 at 9:00 AM in Courtroom 3B [Source 1].",
"refusal": null,
"tool_calls": null,
"citations": [
{
"source_index": 1,
"document_id": "5e8b3a7c-9d14-4f6e-a2b0-7c1d9e3f5a82",
"source_filename": "hearing-notice-2026-CV-1042.pdf",
"chunk_index": 0,
"text_preview": "NOTICE OF HEARING. Case No. 2026-CV-1042. A hearing is scheduled for August 15, 2026 at 9:00 AM in Courtroom 3B.",
"score": 0.93
}
]
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 812,
"completion_tokens": 143,
"total_tokens": 955
}
}
Add -i to see the headers: every response carries X-Request-ID, and
every authenticated /v1 response carries RateLimit-Limit, RateLimit-Remaining, and
RateLimit-Reset (Rate limits). Always call the canonical
path without a trailing slash.
Other OpenAI-compatible clients
Any client that lets you set a base URL and an API key can call the
OpenAI-shaped endpoints: set the base URL to
https://api.chatbots.ecourtdate.com/v1, supply the key as the Bearer token
(most clients call this setting "API key"), and use a bot slug or default
wherever the client asks for a model name. A client that parses only the
standard fields may not expose citations; if you need them through such a
client, read the raw response body for that request or call the endpoint
with a plain HTTP client. Clients that send unsupported parameters still
work because the API ignores them; the exceptions (n other than 1,
tool types other than function, unknown response_format types) are
rejected with 400 and listed in Chat completions.
Retries and timeouts
Both official SDKs retry automatically: two retries by default, with
exponential backoff starting at 0.5 s and capped at 8 s, on connection
errors, 408, 409, 429, and every 5xx. When the response carries
Retry-After and its value is at most 60 seconds, the SDKs wait exactly
that long instead. That matches how the eCourtDate Chatbot API
signals waits (Rate limits):
| Response | Retry-After | What the SDK does | What you should do |
|---|---|---|---|
429 rate_limit_exceeded (per-minute limit) | Seconds left in the current minute (1 to 60) | Waits Retry-After, retries | Let the SDK retry; spread bursts |
429 insufficient_quota (daily token quota) | Seconds until the end of the UTC day | Falls back to short backoff, retries twice, then raises | Do not retry; stop until the quota resets |
429 rate_limit_exceeded (the underlying language model is rate limited) | 10 | Waits 10 s, retries | Let the SDK retry |
503 upstream_unavailable | 5 | Waits 5 s, retries | Let the SDK retry; alert if it persists |
500 server_error | none | Backoff, retries | Let the SDK retry; report the X-Request-ID |
400, 401, 403, 404, 413 | none | Raises immediately | Fix the request; never retry unchanged |
Configure retries per client or per request:
client = OpenAI(
base_url="https://api.chatbots.ecourtdate.com/v1",
api_key=os.environ["API_KEY"],
max_retries=3, # default 2; 0 disables
timeout=60.0, # seconds; default 600 with a 5 s connect timeout
)
# Per request:
client.with_options(max_retries=0, timeout=20.0).models.list()
const client = new OpenAI({
baseURL: "https://api.chatbots.ecourtdate.com/v1",
apiKey: process.env.API_KEY,
maxRetries: 3, // default 2; 0 disables
timeout: 60 * 1000, // milliseconds; default 10 minutes
});
// Per request:
await client.models.list({ maxRetries: 0, timeout: 20 * 1000 });
Two things to keep in mind:
- A chat completion that retrieves from a large knowledge base and generates
a long answer can take tens of seconds. Size
timeoutfor your longest expected answer, or cap answer length withmax_completion_tokens. - Rate limits are per account, shared by every key and every process. If you run several workers, add a client-side limiter so retries do not amplify a burst (Rate limits).
Streaming with the SDKs
Pass stream: true and iterate. Content arrives as deltas; citations
arrive once, on the finish chunk (the chunk whose finish_reason is not
null), because [Source N] markers can be split across deltas. Request
stream_options: {"include_usage": true} to receive a final usage chunk
with an empty choices array.
stream = client.chat.completions.create(
model="default",
messages=[{"role": "user", "content": "What do I bring to jury duty?"}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.usage: # the usage chunk has no choices
print("\ntokens:", chunk.usage.total_tokens)
continue
choice = chunk.choices[0]
if choice.delta.content:
print(choice.delta.content, end="", flush=True)
if choice.finish_reason is not None:
citations = choice.delta.model_extra.get("citations") or []
print("\nsources:", [c["source_filename"] for c in citations])
const stream = await client.chat.completions.create({
model: "default",
messages: [{ role: "user", content: "What do I bring to jury duty?" }],
stream: true,
stream_options: { include_usage: true },
});
for await (const chunk of stream) {
if (chunk.usage) { // the usage chunk has no choices
console.log("\ntokens:", chunk.usage.total_tokens);
continue;
}
const choice = chunk.choices[0];
if (choice.delta.content) process.stdout.write(choice.delta.content);
if (choice.finish_reason !== null) {
const citations = choice.delta.citations ?? [];
console.log("\nsources:", citations.map((c) => c.source_filename));
}
}
Once the stream has started the HTTP status is already 200, so a failure
mid-stream arrives as an in-band frame data: {"error": {...}} followed by
[DONE]. Both SDKs yield the chunks received so far and then raise
APIError (Python) or throw OpenAI.APIError (Node) carrying the frame's
message, type, and code. Branch on type: a server_error or
rate_limit_error frame is retryable, an invalid_request_error frame
(the underlying language model rejected the request) is not
(errors in a stream). A pre-stream failure (bad key,
unknown model, invalid body) is an ordinary HTTP error with a real status
and raises the matching status exception. No usage chunk follows an error
frame. Frame-level details, including how tool-call deltas accumulate, are
in Streaming.
eCourtDate-specific endpoints
Conversations (/v1/conversations), ingestion (/v1/ingest/files),
crawling (/v1/ingest/crawl), documents (/v1/documents), and job status
(/v1/ingest/jobs/{jobId}, /v1/ingest/crawl/{crawlJobId}) have no
OpenAI SDK method. Call them with any HTTP client, sending the same
Authorization: Bearer header. They return the same error envelope, so the
handling in the next section applies to them as well.
import os, requests
BASE = "https://api.chatbots.ecourtdate.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['API_KEY']}"}
# Start a conversation, then add a message (requires the chat scope)
conv = requests.post(f"{BASE}/conversations", headers=HEADERS,
json={"model": "default"}, timeout=30).json()
reply = requests.post(f"{BASE}/conversations/{conv['id']}/messages", headers=HEADERS,
json={"content": "When is my hearing?"}, timeout=120).json()
print(reply["message"]["content"], reply["message"]["citations"])
# Upload a file to the knowledge base (requires the ingest scope)
with open("hearing-notice-2026-CV-1042.pdf", "rb") as f:
job = requests.post(f"{BASE}/ingest/files", headers=HEADERS,
files=[("file", f)], data={"namespace": "general"},
timeout=300).json()
print(job["job_id"], job["status"]) # "processing"
import fs from "node:fs/promises";
const BASE = "https://api.chatbots.ecourtdate.com/v1";
const headers = { Authorization: `Bearer ${process.env.API_KEY}` };
const conv = await fetch(`${BASE}/conversations`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ model: "default" }),
}).then((r) => r.json());
const form = new FormData();
form.append("file", new Blob([await fs.readFile("notice.pdf")]), "notice.pdf");
form.append("namespace", "general");
const job = await fetch(`${BASE}/ingest/files`, { method: "POST", headers, body: form })
.then((r) => r.json());
Request and response shapes for each endpoint are in Conversations, Ingesting files, Crawling websites, Documents, and Jobs and webhooks.
Error handling
Every error is the envelope {"error": {"message", "type", "param", "code"}}.
The SDKs map the HTTP status to an exception class and copy the envelope
fields onto it. Branch on code (stable) and fall back to the status class;
never on message, which may be reworded.
| HTTP | Envelope code | Python SDK exception | Node SDK class |
|---|---|---|---|
400 | null (validation; param names the field) or tool_not_allowed | openai.BadRequestError | OpenAI.BadRequestError |
401 | invalid_api_key | openai.AuthenticationError | OpenAI.AuthenticationError |
403 | insufficient_scope | openai.PermissionDeniedError | OpenAI.PermissionDeniedError |
404 | not_found, model_not_found | openai.NotFoundError | OpenAI.NotFoundError |
413 | request_too_large | openai.APIStatusError | OpenAI.APIError |
429 | rate_limit_exceeded, insufficient_quota | openai.RateLimitError | OpenAI.RateLimitError |
500 | null, with type server_error | openai.InternalServerError | OpenAI.InternalServerError |
503 | upstream_unavailable | openai.InternalServerError | OpenAI.InternalServerError |
| no response | openai.APIConnectionError, openai.APITimeoutError | OpenAI.APIConnectionError, OpenAI.APIConnectionTimeoutError |
Every status exception exposes the envelope and the request id:
| Envelope field | Python SDK attribute | Node SDK property |
|---|---|---|
error.message | e.body["message"] (e.message is prefixed with the status and contains the whole envelope) | e.message (prefixed with the status) |
error.type | e.type | e.type |
error.param | e.param | e.param |
error.code | e.code | e.code |
the error object | e.body | e.error |
| HTTP status | e.status_code | e.status |
X-Request-ID header | e.request_id | e.requestID |
| all headers | e.response.headers | e.headers |
import openai
try:
completion = client.chat.completions.create(
model="traffic-court",
messages=[{"role": "user", "content": "How do I contest a citation?"}],
)
except openai.AuthenticationError as e: # 401 invalid_api_key
raise SystemExit("API key rejected; check the key and its activation")
except openai.PermissionDeniedError as e: # 403 insufficient_scope
raise SystemExit(f"key lacks a scope: {e.body['message']}")
except openai.NotFoundError as e: # 404 model_not_found (param "model")
print(e.body["message"]) # lists the available models
except openai.RateLimitError as e: # 429, already retried by the SDK
wait = e.response.headers.get("Retry-After")
if e.code == "insufficient_quota":
print(f"daily token quota reached; resets in {wait} s")
else:
print(f"rate limited; retry in {wait} s")
except openai.BadRequestError as e: # 400, do not retry
print(f"invalid request: {e.param}: {e.body['message']}")
except openai.APIStatusError as e: # 413, 500, 503 and anything else
print(f"request {e.request_id} failed: {e.status_code} {e.code}")
try {
await client.chat.completions.create({
model: "traffic-court",
messages: [{ role: "user", content: "How do I contest a citation?" }],
});
} catch (err) {
if (err instanceof OpenAI.RateLimitError && err.code === "insufficient_quota") {
// Daily token quota reached; Retry-After is the time until the UTC day ends.
console.error("quota exhausted until", err.headers.get("retry-after"), "s from now");
} else if (err instanceof OpenAI.BadRequestError) {
console.error("fix the request:", err.param, err.message);
} else if (err instanceof OpenAI.APIError) {
console.error(err.status, err.code, err.requestID);
} else {
throw err;
}
}
The complete registry, with the message wording and param for each code,
is in Errors. Include the request id
(Conventions) in any support request.