Conversations
A conversation is a server-side transcript bound to one bot. You create it once, then post each user message to it; the eCourtDate Chatbot API stores the turn, replays the history to the model, and returns the reply with its citations. The full transcript is retrievable at any time.
POST /v1/conversations
GET /v1/conversations/{conversationId}
POST /v1/conversations/{conversationId}/messages
DELETE /v1/conversations/{conversationId}
Scope: chat
These endpoints are specific to the eCourtDate Chatbot API (they are not part
of the OpenAI shape), so call them with plain HTTP rather than the OpenAI
SDKs. Conversation ids are UUIDs in canonical hyphenated form, for example
a06fb97c-92ba-44ae-ad05-7b1a545a22d5, and are visible only to the account
that created them.
Conversations or chat completions?
| Use conversations when | Use chat completions when |
|---|---|
| You want the server to keep the history (a public chat widget, a help desk, a kiosk). | Your application already keeps the history or builds each prompt itself. |
| You want a retrievable transcript for review or support. | You need tools, response_format, or per-request sampling parameters. |
| The bot's configured settings are all you need. | You use the OpenAI SDKs or another OpenAI-compatible client. |
Generation settings for a conversation (persona, token limit, citations)
come from the bot's configuration, and sampling is left to the underlying
language model; the messages endpoint accepts only content and stream.
Create a conversation
POST /v1/conversations returns 201 with the new conversation.
| Field | Type | Description |
|---|---|---|
model | string | A bot slug or alias, resolved exactly as on chat completions; the literal "default" binds the conversation to whatever your account's default is right now, provided that default is a real bot (when no bot is enabled and the default is the built-in assistant, nothing is bound and model is stored as null). Omit it, or send null or an empty string (whitespace is trimmed), to store no binding: the conversation then follows the account default, resolved on each message. |
metadata | object | Up to 16 keys. Keys are strings of 1 to 64 characters; values are strings of at most 512 characters. Stored with the conversation and returned by create and retrieve. Use it for your own identifiers (a session id, a case number, the channel). |
curl -s "https://api.chatbots.ecourtdate.com/v1/conversations" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "court-assistant",
"metadata": { "channel": "web-widget", "session_id": "sess_8f21c4" }
}'
{
"id": "a06fb97c-92ba-44ae-ad05-7b1a545a22d5",
"object": "conversation",
"model": "court-assistant",
"metadata": { "channel": "web-widget", "session_id": "sess_8f21c4" },
"created": 1787360082,
"updated": 1787360082
}
| Field | Type | Description |
|---|---|---|
id | string | The conversation id. Store it; there is no list endpoint. |
object | string | Always conversation. |
model | string or null | The canonical slug of the bound bot (also when you sent an alias or "default"), or null when the conversation follows the account default (including "default" sent while the built-in assistant is the default). |
metadata | object | As sent; {} when none was sent. |
created | integer | Unix time in seconds (UTC) when the conversation was created. |
updated | integer | Unix time in seconds (UTC) of the last stored turn; equal to created until the first message. |
An empty object {}, an empty body, or no body at all is valid and creates
a conversation with model: null that follows the account default.
A model that is unknown, disabled, or belongs to another account is
rejected at create time with 404 model_not_found; omitting model when
several bots are enabled and no default is configured is rejected with 400
and param: "model".
Add a message
POST /v1/conversations/{conversationId}/messages appends a user message,
generates the reply, stores both, and returns the reply.
| Field | Type | Description |
|---|---|---|
content | string | Required. 1 to 100,000 characters (counted as Unicode characters). Must not be empty or whitespace-only (400, param: "content"); otherwise it is not trimmed and is sent to the model as is. |
stream | boolean | Default false. true returns server-sent events. Must be a JSON boolean. |
Unknown fields are ignored.
curl -s "https://api.chatbots.ecourtdate.com/v1/conversations/a06fb97c-92ba-44ae-ad05-7b1a545a22d5/messages" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "content": "What time does jury duty start?" }'
{
"conversation_id": "a06fb97c-92ba-44ae-ad05-7b1a545a22d5",
"message": {
"role": "assistant",
"content": "Jurors report to the jury assembly room on the second floor by 8:30 a.m. [Source 1].",
"citations": [
{
"source_index": 1,
"document_id": "2f6e1c0a-8d3b-4c57-9a41-5b2e8d7f0c13",
"source_filename": "juror-handbook.pdf",
"chunk_index": 0,
"text_preview": "Report to the jury assembly room on the second floor by 8:30 a.m. Parking is validated for jurors in the Fifth Street garage.",
"score": 0.91
}
]
}
}
| Field | Type | Description |
|---|---|---|
conversation_id | string | The conversation the turn was added to. |
message.role | string | Always assistant. |
message.content | string | The reply, with [Source N] markers left in place. |
message.citations | array or null | Citation objects: an array (empty when nothing was cited) when the bot has a knowledge base and citations enabled, null otherwise. |
The body carries no usage, model, or finish_reason. Token usage still
counts toward your daily quota.
A second message continues the exchange; the server supplies the history:
curl -s "https://api.chatbots.ecourtdate.com/v1/conversations/a06fb97c-92ba-44ae-ad05-7b1a545a22d5/messages" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "content": "Is parking free?" }'
{
"conversation_id": "a06fb97c-92ba-44ae-ad05-7b1a545a22d5",
"message": {
"role": "assistant",
"content": "Parking is validated for jurors in the Fifth Street garage, so you will not pay for the day [Source 1].",
"citations": [
{
"source_index": 1,
"document_id": "2f6e1c0a-8d3b-4c57-9a41-5b2e8d7f0c13",
"source_filename": "juror-handbook.pdf",
"chunk_index": 0,
"text_preview": "Report to the jury assembly room on the second floor by 8:30 a.m. Parking is validated for jurors in the Fifth Street garage.",
"score": 0.89
}
]
}
}
How a turn is processed
- The call is synchronous: the reply is generated before the
200is returned, and the user message and the reply are stored together. AGETimmediately afterwards shows both. - The model receives the stored history plus the new message, with citations stripped from earlier turns. Up to 40 of the most recent messages are sent to the model; the window always opens on a user turn, so it can hold 39. Older messages stay in the transcript but no longer influence replies. Start a new conversation when the topic changes.
- Knowledge-base retrieval uses only the new message as the query, exactly as on chat completions.
- The bot is resolved again on every message from the conversation's stored
model. If that bot has since been disabled, the message is rejected with404model_not_foundwhileGETstill returns the transcript. A conversation created without amodelfollows whatever the account default is at the time of each message; if that default has become ambiguous (several enabled bots and no configured default), the message is rejected with400andparam: "model". - A failed turn is not stored: after a
4xx,5xx, or a streaming error frame, the transcript is unchanged and the message can simply be resent.
Streamed turns deliver the same reply as delta frames followed by a done
frame that carries the final message. See
Conversation streams.
Retrieve a conversation
GET /v1/conversations/{conversationId} returns the transcript.
curl -s "https://api.chatbots.ecourtdate.com/v1/conversations/a06fb97c-92ba-44ae-ad05-7b1a545a22d5" \
-H "Authorization: Bearer $API_KEY"
{
"id": "a06fb97c-92ba-44ae-ad05-7b1a545a22d5",
"object": "conversation",
"model": "court-assistant",
"messages": [
{
"role": "user",
"content": "What time does jury duty start?",
"citations": null
},
{
"role": "assistant",
"content": "Jurors report to the jury assembly room on the second floor by 8:30 a.m. [Source 1].",
"citations": [
{
"source_index": 1,
"document_id": "2f6e1c0a-8d3b-4c57-9a41-5b2e8d7f0c13",
"source_filename": "juror-handbook.pdf",
"chunk_index": 0,
"text_preview": "Report to the jury assembly room on the second floor by 8:30 a.m. Parking is validated for jurors in the Fifth Street garage.",
"score": 0.91
}
]
},
{
"role": "user",
"content": "Is parking free?",
"citations": null
},
{
"role": "assistant",
"content": "Parking is validated for jurors in the Fifth Street garage, so you will not pay for the day [Source 1].",
"citations": [
{
"source_index": 1,
"document_id": "2f6e1c0a-8d3b-4c57-9a41-5b2e8d7f0c13",
"source_filename": "juror-handbook.pdf",
"chunk_index": 0,
"text_preview": "Report to the jury assembly room on the second floor by 8:30 a.m. Parking is validated for jurors in the Fifth Street garage.",
"score": 0.89
}
]
}
],
"created": 1787360082,
"updated": 1787360174,
"metadata": { "channel": "web-widget", "session_id": "sess_8f21c4" }
}
| Field | Type | Description |
|---|---|---|
id, object, model, metadata | As on create. model is null for a conversation created without one. | |
messages | array | Every stored message in order, alternating user and assistant. User messages always have citations: null. Per-message timestamps are not exposed. |
created | integer | Unix time in seconds (UTC) when the conversation was created. Equal to the created value returned by the create call. |
updated | integer | Unix time in seconds (UTC) of the last stored turn. Always >= created; equal to it until the first message. |
There is no list endpoint and no way to edit or delete individual messages:
keep your own index of conversation ids (for example in metadata of your
session store) and treat a transcript as append-only.
Delete a conversation
DELETE /v1/conversations/{conversationId} permanently removes the
transcript and returns 204 with an empty body and no Content-Type
header.
curl -s -o /dev/null -w "%{http_code}\n" -X DELETE \
"https://api.chatbots.ecourtdate.com/v1/conversations/a06fb97c-92ba-44ae-ad05-7b1a545a22d5" \
-H "Authorization: Bearer $API_KEY"
# 204
Deletion is immediate and irreversible. Afterwards GET, DELETE, and
POST .../messages on the id all return 404
not_found, so a repeated delete is not a no-op at
the status level; treat a 404 after your own delete as success.
Isolation
Conversations belong to the account whose key created them. An id from
another account, a deleted id, a malformed id, and an id that never existed
all produce the same 404 not_found body, so a response never reveals
whether a conversation exists elsewhere.
{
"error": {
"message": "Conversation not found.",
"type": "invalid_request_error",
"param": null,
"code": "not_found"
}
}
Full example
A minimal help-desk loop in Python with the requests library:
import os
import requests
BASE = "https://api.chatbots.ecourtdate.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['API_KEY']}"}
conversation = requests.post(
f"{BASE}/conversations",
headers=HEADERS,
json={"model": "court-assistant", "metadata": {"channel": "phone-tree", "caller": "anon-4471"}},
)
conversation.raise_for_status()
conversation_id = conversation.json()["id"]
for question in ["What time does jury duty start?", "Is parking free?"]:
reply = requests.post(
f"{BASE}/conversations/{conversation_id}/messages",
headers=HEADERS,
json={"content": question},
)
reply.raise_for_status()
message = reply.json()["message"]
print(question)
print(" ->", message["content"])
for citation in message["citations"] or []:
print(" source:", citation["source_filename"], "chunk", citation["chunk_index"])
transcript = requests.get(f"{BASE}/conversations/{conversation_id}", headers=HEADERS).json()
print(len(transcript["messages"]), "messages stored")
requests.delete(f"{BASE}/conversations/{conversation_id}", headers=HEADERS)
The same in Node with fetch:
const BASE = "https://api.chatbots.ecourtdate.com/v1";
const headers = {
Authorization: `Bearer ${process.env.API_KEY}`,
"Content-Type": "application/json",
};
const created = await fetch(`${BASE}/conversations`, {
method: "POST",
headers,
body: JSON.stringify({ model: "court-assistant", metadata: { channel: "web-widget" } }),
});
const { id } = await created.json();
const reply = await fetch(`${BASE}/conversations/${id}/messages`, {
method: "POST",
headers,
body: JSON.stringify({ content: "What time does jury duty start?" }),
});
const { message } = await reply.json();
console.log(message.content, message.citations);
await fetch(`${BASE}/conversations/${id}`, { method: "DELETE", headers });
Errors
| Status | code | Operations | When |
|---|---|---|---|
400 | null | create, messages | Validation: content missing, empty, whitespace-only, or over 100,000 characters (param: "content"); stream not a boolean (param: "stream"); metadata that is not an object or has more than 16 keys (param: "metadata"), or a key over 64 characters, a non-string value, or a value over 512 characters (param: "metadata.<key>"); model not a string (param: "model"); a body that is not a JSON object (param: null; on create, a missing or empty body counts as {}). Also ambiguous model resolution (param: "model"), on create and on a message to a conversation that follows the account default. |
401 | invalid_api_key | all | Missing or invalid key. |
403 | insufficient_scope | all | The key lacks the chat scope. |
404 | not_found | retrieve, messages, delete | Unknown, deleted, malformed, or another account's conversation id. |
404 | model_not_found | create, messages | The model (or the conversation's stored bot) is unknown, disabled, or inaccessible; param: "model". |
413 | request_too_large | create, messages | Body over 5 MiB. |
429 | rate_limit_exceeded | all | Per-minute request limit reached (retrieve and delete count too), or the underlying language model is rate limited. Honor Retry-After. |
429 | insufficient_quota | all | Daily token quota reached. |
500 | null | messages | type: server_error. Unexpected failure; the turn was not stored. Retry with backoff. |
503 | upstream_unavailable | messages | The underlying language model or the text embedding service (used for retrieval) failed or timed out; the turn was not stored. Retry after Retry-After. |
With stream: true, these statuses apply only before the stream starts;
later failures arrive as an in-band error frame (Streaming).
All errors use the envelope described in Errors.