Chat completions
POST /v1/chat/completions is the main endpoint of the eCourtDate Chatbot
API. It accepts the OpenAI chat completions request shape, answers with a
bot configured for your account, and grounds the answer in your knowledge
base when the bot has one. Official OpenAI SDKs work unchanged once pointed
at the base URL; the only additions are the citations extension on the
response and a few account-level limits described below.
POST https://api.chatbots.ecourtdate.com/v1/chat/completions
Scope: chat
A first request
curl -s "https://api.chatbots.ecourtdate.com/v1/chat/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "court-assistant",
"messages": [
{ "role": "user", "content": "What are the Traffic Division hours?" }
]
}'
{
"id": "chatcmpl-c14a12e8a6a64e9283a217e7",
"object": "chat.completion",
"created": 1787360841,
"model": "court-assistant",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The Traffic Division is open Monday through Friday, 8:00 a.m. to 4:30 p.m., at the Main Street courthouse [Source 1].",
"refusal": null,
"tool_calls": null,
"citations": [
{
"source_index": 1,
"document_id": "17136f01-972d-4956-868e-8159833072e4",
"source_filename": "traffic-division-faq.pdf",
"chunk_index": 2,
"text_preview": "Traffic Division hours: Monday through Friday, 8:00 a.m. to 4:30 p.m. Located on the first floor of the Main Street courthouse.",
"score": 0.93
}
]
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 812,
"completion_tokens": 143,
"total_tokens": 955
}
}
The same call with the official SDKs:
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["API_KEY"],
base_url="https://api.chatbots.ecourtdate.com/v1",
)
completion = client.chat.completions.create(
model="court-assistant",
messages=[{"role": "user", "content": "What are the Traffic Division hours?"}],
)
print(completion.choices[0].message.content)
# The citations extension is available on the parsed object's extra fields:
print(completion.choices[0].message.model_extra.get("citations"))
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.API_KEY,
baseURL: "https://api.chatbots.ecourtdate.com/v1",
});
const completion = await client.chat.completions.create({
model: "court-assistant",
messages: [{ role: "user", content: "What are the Traffic Division hours?" }],
});
console.log(completion.choices[0].message.content);
console.log(completion.choices[0].message.citations); // extension field
Request
The body is application/json and must be a JSON object. Unknown top-level
fields are ignored. Values must be JSON-typed: a boolean is true, not
"true", and a number is 0.5, not "0.5".
| Field | Type | Description |
|---|---|---|
messages | array | Required. 1 to 200 messages. Each message's text may be at most 100,000 characters. |
model | string | A bot slug or alias from GET /v1/models. Omit it, send null, or send "default" to use your account's default model. Surrounding whitespace is trimmed and an empty string is treated as omitted. |
stream | boolean | Default false. true switches the response to server-sent events. |
stream_options | object | { "include_usage": true } adds a final usage chunk to a stream. Validated on every request (include_usage must be a JSON boolean) but acted on only when stream is true. |
temperature | number | 0 to 2. Values above 1 are treated as 1 by the underlying language model. Omit to use the model's default. |
top_p | number | 0 to 1. Ignored when temperature is also set, so send one or the other. Omit to use the model's default. |
max_completion_tokens | integer | Upper bound on generated tokens, at least 1. Values above 8192 are clamped to 8192, not rejected. Omit to use the bot's configured limit. |
max_tokens | integer | Legacy alias of max_completion_tokens with the same bounds. When both are sent, max_completion_tokens wins. |
stop | string or array of strings | Sequences at which generation stops. Every string must be non-empty ("" and [""] are rejected with 400); up to 4 sequences are honored. |
n | integer | Must be 1 (the default). Any other value is rejected with 400. |
tools | array | Function definitions the model may call. An empty array is the same as omitting the field. See Tools and structured output. |
tool_choice | string or object | "auto", "none", "required", or a forced function. Validated after the message rules and the tool definitions, even without tools (a malformed value is 400); a well-formed value is ignored when no tools are sent. See Tools and structured output. |
response_format | object | text, json_object, or json_schema. See Structured output. |
user | string | Accepted for compatibility and ignored. Must be a string if present. |
Accepted and ignored fields
These OpenAI fields are accepted so that existing client code keeps working,
but they have no effect: seed, presence_penalty, frequency_penalty,
logit_bias, logprobs, top_logprobs, parallel_tool_calls, metadata,
store, service_tier, reasoning_effort, modalities, audio,
prediction, web_search_options, and the per-message name.
temperature and top_p are the only sampling controls. Bots carry no
sampling defaults of their own (only a token limit), so an omitted value is
left to the underlying language model, and some models ignore sampling
parameters altogether.
Messages
Each message is { "role", "content", ... }. The roles are system,
developer, user, assistant, and tool; any other role is rejected with
400.
| Role | content | Notes |
|---|---|---|
system, developer | string | Instructions from your application. Must not be empty or whitespace-only (400, param: messages[i].content). Layered after the bot's persona; see below. |
user | string, or an array of { "type": "text", "text": "..." } parts | Must not be empty or whitespace-only. Text parts are joined with a newline. A part whose type is not text (for example image_url) is rejected with 400 and param: messages[i].content[j].type; a text part without a string text with param: messages[i].content[j].text. |
assistant | string, or null when tool_calls is present | A previous reply you are replaying. Must not be empty unless it carries tool_calls. |
tool | string, an array of text parts, null, or "" | The result of a function call (null or "" means an empty result). tool_call_id is required and must answer a tool call from the nearest preceding assistant message. See the tool loop. |
The conversation as a whole must satisfy four rules, each enforced with a
400:
- at least one message other than
systemordeveloperis present (param: "messages"); - the first message that is not
systemordeveloperhas roleuser: a leadingassistantmessage is rejected withparam: "messages", and a leadingtoolmessage by the rule below; - the last message has role
userortool(param: "messages"); - every
toolmessage answers a tool call: itstool_call_idmust match anidin thetool_callsof the nearest precedingassistantmessage, and each call may be answered at most once. Otherwise the request is rejected withparam: "messages[i].tool_call_id"(the index of thetoolmessage) and the messagetool_call_id does not match a preceding tool call.; atoolmessage with no assistant tool call before it at all gets the same error.
Consecutive user messages are allowed and are forwarded as sent.
System and developer messages
Every bot has a persona configured by eCourtDate for your account: a name,
instructions, tone, and the grounding rules that make it cite your knowledge
base. Your system and developer messages do not replace that persona. They
are collected from anywhere in messages, concatenated in order of
appearance, and appended to the persona as additional instructions from the
API caller. Use them for request-specific guidance ("answer in Spanish",
"keep it under three sentences"), not to redefine what the bot is.
{
"model": "court-assistant",
"messages": [
{ "role": "system", "content": "Answer in Spanish." },
{ "role": "user", "content": "How do I pay a filing fee online?" }
]
}
Model resolution
model names a bot (by slug) or an alias configured for your account. The
response always reports the bot's canonical slug, even when you sent an
alias. Lookups are case-sensitive.
| You send | Result |
|---|---|
| An enabled bot slug | That bot. |
| An alias | The bot the alias points to; the response model is the bot's slug. |
Omitted, null, "", or "default" | Your account's default bot. If no default is set and exactly one bot is enabled, that bot is used; if no bot is enabled, the built-in assistant answers. See The default model. |
| Omitted with several enabled bots and no default | 400 with param: "model" and code: null. The message asks you to pass model explicitly or to have eCourtDate set a default for your account. |
| Unknown, disabled, or another account's slug | 404 model_not_found with param: "model". The message lists the models available to you and ends with Omit the 'model' field to use this account's default. when a default resolves, or Pass one of them. when the default is ambiguous. |
{
"error": {
"message": "Model 'traffic-bot' was not found. Available models: court-assistant, jury-helpdesk. Omit the 'model' field to use this account's default.",
"type": "invalid_request_error",
"param": "model",
"code": "model_not_found"
}
}
Branch on code and param, never on the message text. See
Models for the list and retrieve endpoints and how bots are
configured.
Grounding and retrieval
When the bot has a knowledge base, the API embeds the text of the last
user message, retrieves the most relevant passages from the bot's
namespace (optionally restricted to specific documents, as configured on the
bot), and hands them to the model with the instruction to cite them as
[Source N]. Earlier turns are passed through unchanged and are not used as
retrieval queries. Retrieval is skipped when the last message is a tool
result; it runs again on the next user message.
A bot without a knowledge base answers from the model's general knowledge and
its citations field is always null. See Citations for the
marker format and the citations array.
Response
A non-streaming call returns 200 with a chat.completion object.
| Field | Type | Description |
|---|---|---|
id | string | chatcmpl- followed by 24 lowercase hex characters. |
object | string | Always chat.completion. |
created | integer | Unix time in seconds (UTC) when the completion was created. |
model | string | The canonical slug of the bot that answered. |
choices | array | Exactly one choice (n is always 1). |
choices[0].index | integer | Always 0. |
choices[0].message | object | The assistant message, below. |
choices[0].logprobs | null | Always null. |
choices[0].finish_reason | string | stop, length, content_filter, or tool_calls. See below. |
usage.prompt_tokens | integer | Tokens in the prompt, including the persona and retrieved passages. |
usage.completion_tokens | integer | Tokens generated. |
usage.total_tokens | integer | prompt_tokens + completion_tokens. Counts toward your daily token quota. |
The message object. Every key is always present, null where it does not
apply, as in the OpenAI object:
| Field | Type | Description |
|---|---|---|
role | string | Always assistant. |
content | string or null | The answer text. "" when the model produced no text; null on a turn that only calls tools. |
refusal | null | Always null. A refusal is returned as ordinary content with finish_reason: "content_filter". |
tool_calls | array or null | The functions the model called, or null when it called none. See Tools and structured output. |
citations | array or null | eCourtDate extension. An array (possibly empty) on a text answer from a bot with a knowledge base and citations enabled; null on a tool-call turn and for bots that do not cite. See Citations. |
There is no system_fingerprint, service_tier, prompt_tokens_details, or
completion_tokens_details.
finish_reason
| Value | Meaning |
|---|---|
stop | The model finished its answer or hit one of your stop sequences. |
length | Generation stopped at the token limit (max_completion_tokens or the bot's limit). The answer is truncated. |
content_filter | The model declined to answer. Its explanation is in content. |
tool_calls | The model wants you to run the functions in message.tool_calls. This value is set whenever tool_calls is non-empty. |
Response headers
Every response carries X-Request-ID (echoed from your request when you
send one, otherwise minted); quote it when contacting support. Authenticated
responses under /v1 also carry RateLimit-Limit, RateLimit-Remaining,
and RateLimit-Reset for the per-minute request window. See
Conventions and Rate limits.
Multi-turn requests
The endpoint is stateless: send the whole exchange every time. Replay prior
assistant replies as assistant messages (drop the citations field; it is
ignored if present) and end with the new user message.
curl -s "https://api.chatbots.ecourtdate.com/v1/chat/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "court-assistant",
"messages": [
{ "role": "user", "content": "What are the Traffic Division hours?" },
{ "role": "assistant", "content": "The Traffic Division is open Monday through Friday, 8:00 a.m. to 4:30 p.m., at the Main Street courthouse [Source 1]." },
{ "role": "user", "content": "Is it open on Juneteenth?" }
]
}'
Only the last user message ("Is it open on Juneteenth?") is used as the retrieval query, so phrase follow-ups so they stand on their own, or restate the subject. If you would rather have the server keep the history, use Conversations.
Limits
| Limit | Value |
|---|---|
| Messages per request | 1 to 200 |
| Characters per message | 100,000 |
| Request body | 5,242,880 bytes (5 MiB), inclusive |
| Generated tokens per request | 8192 (max_completion_tokens is clamped) |
| Choices | 1 |
Validation order
Checks run in a fixed order, and only the first failure is reported:
- Body size (
413), then JSON decoding (400withparam: nullfor malformed JSON), then authentication (401), scope (403), and rate limits (429). These run before the body is validated. - Schema validation (
400,paramnames the field, for examplemessages[0].roleortemperature;paramisnullfor a body that is not a JSON object). n, thenstop, thenresponse_format, then model lookup (404), then the message rules, then tool definitions,tool_choice, and the bot's tool allowlist, then retrieval and generation.
Requests must be sent with Content-Type: application/json; a JSON body
sent as text/plain is rejected with 400.
Errors
| Status | code | When |
|---|---|---|
400 | null | Validation failure. param names the offending field in bracket form (messages[2].tool_call_id, tools[0], stop), or is null for malformed JSON. Also n other than 1, empty stop strings, the message rules above (messages, or messages[i].tool_call_id for a tool message that answers no preceding tool call), and ambiguous model resolution (param: "model"). |
400 | tool_not_allowed | A tool in tools is not on the bot's allowlist; param is tools[i]. |
401 | invalid_api_key | Missing or invalid key. Response carries WWW-Authenticate: Bearer. |
403 | insufficient_scope | The key lacks the chat scope. |
404 | model_not_found | Unknown, disabled, or inaccessible model; param is model. |
413 | request_too_large | Body over 5 MiB. |
429 | rate_limit_exceeded | Per-minute request limit reached, or the underlying language model is rate limited. Honor Retry-After. |
429 | insufficient_quota | Daily token quota reached. Retry-After counts down to the next UTC day. |
500 | null | type: server_error. An unexpected failure. Retry with backoff; quote X-Request-ID to support. |
503 | upstream_unavailable | The underlying language model or the text embedding service (used for retrieval) failed or timed out. Nothing was charged; retry after Retry-After. |
All errors use the envelope {"error": {"message", "type", "param", "code"}};
see Errors. With stream: true, these statuses apply only
before the stream starts; failures after that arrive in-band, as described
in Streaming.