Skip to main content

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:

OperationSDK call (Python / Node)
POST /v1/chat/completionsclient.chat.completions.create(...)
GET /v1/modelsclient.models.list()
GET /v1/models/{modelId}client.models.retrieve(...)
POST /v1/embeddingsclient.embeddings.create(...)

Within that surface:

  • Every response parses. Success bodies are the OpenAI objects (chat.completion, chat.completion.chunk, model, embedding, and the list envelope). 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; n must be 1. See Chat completions for the supported set.
  • model names a bot. The value is a bot slug or alias configured for your account, or default for the account default (omitting it does the same). Responses echo the canonical slug; an unknown name is 404 model_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:
    • citations on assistant messages: the knowledge-base passages the answer was drawn from, an array (possibly empty) on a text answer from a bot that cites and null on tool-call turns and for bots that do not (Citations). In a stream it arrives on the finish chunk (Streaming).
    • input_type on embeddings requests (search_document or search_query), sent through the SDK's extra_body (Embeddings).
  • Tool call ids are opaque strings. Do not parse or generate them; echo them back unchanged as tool_call_id in tool messages (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):

ResponseRetry-AfterWhat the SDK doesWhat you should do
429 rate_limit_exceeded (per-minute limit)Seconds left in the current minute (1 to 60)Waits Retry-After, retriesLet the SDK retry; spread bursts
429 insufficient_quota (daily token quota)Seconds until the end of the UTC dayFalls back to short backoff, retries twice, then raisesDo not retry; stop until the quota resets
429 rate_limit_exceeded (the underlying language model is rate limited)10Waits 10 s, retriesLet the SDK retry
503 upstream_unavailable5Waits 5 s, retriesLet the SDK retry; alert if it persists
500 server_errornoneBackoff, retriesLet the SDK retry; report the X-Request-ID
400, 401, 403, 404, 413noneRaises immediatelyFix 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 timeout for your longest expected answer, or cap answer length with max_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.

HTTPEnvelope codePython SDK exceptionNode SDK class
400null (validation; param names the field) or tool_not_allowedopenai.BadRequestErrorOpenAI.BadRequestError
401invalid_api_keyopenai.AuthenticationErrorOpenAI.AuthenticationError
403insufficient_scopeopenai.PermissionDeniedErrorOpenAI.PermissionDeniedError
404not_found, model_not_foundopenai.NotFoundErrorOpenAI.NotFoundError
413request_too_largeopenai.APIStatusErrorOpenAI.APIError
429rate_limit_exceeded, insufficient_quotaopenai.RateLimitErrorOpenAI.RateLimitError
500null, with type server_erroropenai.InternalServerErrorOpenAI.InternalServerError
503upstream_unavailableopenai.InternalServerErrorOpenAI.InternalServerError
no responseopenai.APIConnectionError, openai.APITimeoutErrorOpenAI.APIConnectionError, OpenAI.APIConnectionTimeoutError

Every status exception exposes the envelope and the request id:

Envelope fieldPython SDK attributeNode SDK property
error.messagee.body["message"] (e.message is prefixed with the status and contains the whole envelope)e.message (prefixed with the status)
error.typee.typee.type
error.parame.parame.param
error.codee.codee.code
the error objecte.bodye.error
HTTP statuse.status_codee.status
X-Request-ID headere.request_ide.requestID
all headerse.response.headerse.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.