Skip to main content

Using the API with AI agents

Coding assistants and AI agents integrate fastest when they are handed the API's contract up front instead of guessing it from the OpenAI docs. Three resources exist for exactly that:

  • llms.txt: a compact, plain-text summary of the API at a stable URL, following the llms.txt convention.
  • The OpenAPI specification (also as YAML): the complete machine-readable contract, including the streaming chunk schema, the citations extension, the error envelope, and the webhook payloads. Agents can read it directly or you can load it into a tool.
  • The onboarding prompt below: paste it into your assistant at the start of an integration task.

The one thing an agent cannot infer from the OpenAI docs is where this API differs: bots instead of model names, the citations extension, the eCourtDate-specific knowledge base and conversation endpoints, and the per-account limits. The prompt covers those.

Onboarding prompt

You are integrating with the eCourtDate Chatbot API.

Facts:
- Base URL: https://api.chatbots.ecourtdate.com/v1 (every endpoint is under
/v1; this exact value is the OpenAI SDK base_url). GET /health (outside
/v1) is the only unauthenticated operation.
- Auth: every request sends `Authorization: Bearer $API_KEY`. Keys look like
`ecd_sk_...` and carry scopes: `chat` (chat completions, models,
embeddings, conversations) and `ingest` (file ingestion, crawling,
documents, jobs). Access requires a paid subscription; keys are issued and
activated for the account by eCourtDate. There is no self-serve key page.
Read the key from configuration; never print or hardcode it. Keys are
server-side secrets: do not call the API from a browser.
- OpenAI compatibility: POST /v1/chat/completions, GET /v1/models,
GET /v1/models/{modelId}, and POST /v1/embeddings use the OpenAI request
and response shapes. The official OpenAI Python and Node SDKs work with
only base_url and api_key changed. Unknown request fields are ignored;
`n` must be 1.
- `model` names a bot (slug) or alias configured for the account; omit it
(or send null, "", or "default") for the account default. Responses echo
the canonical slug. GET /v1/models lists what the key can use. An
unknown or disabled model is 404 `model_not_found` (param "model"), and
the message lists the available models. Omitting `model` when the account
has several bots and no default is a 400 (param "model").
- Grounded answers: a bot answers from the account's knowledge base.
The assistant message always carries a `citations` key (an eCourtDate
extension): an array (possibly empty) of objects with source_index,
document_id, source_filename, chunk_index, text_preview, score 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. The answer text contains
matching `[Source N]` markers. Every message key is always present (role,
content, refusal, tool_calls, citations; null where it does not apply).
In the OpenAI Python SDK read `message.model_extra.get("citations")`; in
Node the field is on the parsed object.
- Streaming: `stream: true` returns text/event-stream; frames are
`data: <JSON>` chat.completion.chunk objects ending with `data: [DONE]`.
`citations` arrive on the finish chunk (delta.citations, an array when
the bot cites, absent otherwise); `[Source N]` markers can straddle
deltas, so never parse them from partial text.
`stream_options: {"include_usage": true}` adds a final usage chunk with
empty `choices`. A mid-stream failure is an in-band
`data: {"error": {...}}` frame under HTTP 200, followed by
`data: [DONE]`; the SDKs raise APIError. Conversation streams send
{conversation_id, delta{content}} frames, then one {conversation_id,
message, done: true} frame, then `data: [DONE]`.
- Tools: function tools only (`type: "function"`); `tool_choice` is auto,
none, required, or {type: "function", function: {name}}; tool call ids
are opaque strings, echoed back as tool_call_id on `tool` messages; each
tool_call_id must match an unanswered call in the nearest preceding
assistant message (400, param messages[i].tool_call_id). Every
tool must be on the bot's allowlist: 400 `tool_not_allowed` (param
"tools[i]"). `response_format` type is text, json_object, or json_schema
(json_schema.name and json_schema.schema required); the JSON modes cannot
be combined with tools (400, param "response_format").
- Embeddings: POST /v1/embeddings returns 1024-dimensional vectors;
encoding_format float (default) or base64; `input` is one string or an
array of 1 to 96 strings, each 1 to 100,000 characters (an empty string
is 400); `dimensions`, if sent, must be the integer 1024; `model` is an
echo-only label; the extension `input_type` is "search_document"
(default) or "search_query".
- Conversations (server-side memory, chat scope): POST /v1/conversations
(201; body {model?, metadata?: up to 16 string pairs, keys 1 to 64
characters, values up to 512; returns {id, object: "conversation", model
(canonical slug or null), metadata, created, updated}); POST
/v1/conversations/{conversationId}/messages (body {content: 1 to 100,000
characters, stream?}; returns {conversation_id, message{role, content,
citations}}); GET /v1/conversations/{conversationId} (adds messages[]);
DELETE /v1/conversations/{conversationId} (204, no body). The full
history stays retrievable, but the model sees up to 40 of the most
recent messages. Unknown ids are 404 `not_found`.
- Knowledge base (ingest scope): POST /v1/ingest/files (multipart, part
name `file` repeated per file, optional `namespace`, default "general";
returns {job_id, document_ids, status: "processing"}); GET
/v1/ingest/jobs/{jobId}; POST /v1/ingest/crawl (body {namespace,
seed_urls, allowed_domains?, max_pages?, max_depth?, rate_limit_rps?,
respect_robots_txt?}; returns {crawl_job_id, status: "processing"}); GET
/v1/ingest/crawl/{crawlJobId}; GET /v1/documents (cursor list: limit,
after, namespace, status, source_type); GET and DELETE
/v1/documents/{documentId} (delete returns {id, object:
"document.deleted", deleted: true, chunks_deleted}). Job status is
processing | completed | completed_with_errors | failed; document status
is processing | ready | failed. Poll until terminal or receive the
`ingest.completed` / `crawl.completed` webhooks. Unknown ids are 404
`not_found`. Namespaces match ^[a-z0-9][a-z0-9_-]{0,63}$; uploads accept
.csv, .docx, .eml, .htm, .html, .md, .pdf, .txt, .xlsx.
- Limits: JSON request body 5 MiB (5,242,880 bytes); the multipart body of
POST /v1/ingest/files 525,336,576 bytes (20 files of 25 MiB plus 1 MiB of
overhead); over either is 413 `request_too_large`. 1 to 200 messages per
chat completion; 100,000 characters per message; 1 to 20 files per upload
and 25 MiB (26,214,400 bytes) per file, filenames at most 255 bytes with
no path separators; seed_urls 1 to 100; max_pages 1 to 500 (default 50);
max_depth 1 to 10 (default 3); rate_limit_rps 0.1 to 10 (default 2); 1
to 96 embedding inputs per request; GET /v1/documents limit 1 to 1000
(default 100); max_completion_tokens above 8192 is clamped to 8192.
Rate limits are per account, shared by all keys: requests per minute and
tokens per UTC day (429 `rate_limit_exceeded` / `insufficient_quota`).
- Errors: always `{"error": {"message", "type", "param", "code"}}`. `type`
is authentication_error (401), invalid_request_error (400, 403, 404, 405,
413), rate_limit_error (429), or server_error (500, 503). The nine codes:
400 tool_not_allowed; 401 invalid_api_key (message "Incorrect API key
provided.", plus WWW-Authenticate: Bearer); 403 insufficient_scope; 404
model_not_found or not_found; 413 request_too_large; 429
rate_limit_exceeded or insufficient_quota (plus Retry-After); 503
upstream_unavailable (plus Retry-After). `code` is null on 400 validation
errors (param in bracket form such as messages[2].tool_call_id) and on
500. Branch on `code`, then on the status class; never on `message`.
- Headers: every response carries X-Request-ID (send your own, 1 to 64
characters of [A-Za-z0-9_-], and it is echoed; quote it to support).
Every authenticated /v1 response carries RateLimit-Limit,
RateLimit-Remaining, and RateLimit-Reset (not 401, 403, 413, or routing
errors). Respect Retry-After on 429 and 503; never retry 400, 401, 403,
404, or 413 unchanged.
- Webhooks: ingest.completed and crawl.completed are POSTed as JSON, once
per job when it reaches a terminal status, to the account's configured
URL; the body mirrors the job status object plus `event` and `namespace`.
Headers: X-ECD-Event, X-ECD-Delivery-Id (a UUID, identical on every
retry), and X-ECD-Signature: t=<unix seconds>,v1=<hex>, where v1 is the
hexadecimal HMAC-SHA256 over "<t>.<raw body>" keyed with the account's
webhook signing secret (present only when a secret is configured;
recomputed on every attempt). Verify the signature over the raw bytes,
reject stale timestamps, deduplicate on the delivery id, and respond 2xx
within 10 seconds (redirects are not followed). Delivery is best-effort:
three attempts (immediately, then 30 s and 5 min later), then dropped;
poll the job endpoint as the source of truth.

References:
- llms.txt: https://docs.chatbots.ecourtdate.com/llms.txt
- OpenAPI spec: https://docs.chatbots.ecourtdate.com/openapi.json (source of
truth for every path, schema, and example)
- Guides: https://docs.chatbots.ecourtdate.com/guides
- Error registry: https://docs.chatbots.ecourtdate.com/guides/errors

Rules:
- Do not invent endpoints, fields, headers, or codes: everything is in the
OpenAPI spec.
- Do not name or assume the underlying language model or embedding model;
the API does not expose them.
- Use the official OpenAI SDK for the OpenAI-shaped endpoints and a plain
HTTP client for the rest; send JSON-typed values (booleans, not "true").
- Treat the error code set as open: handle unknown codes by status class.

Practices for agent-driven integrations

  • Give the agent the spec, not screenshots. The spec carries every schema constraint (enums, bounds, defaults, the streaming chunk shape) that prose summaries drop.
  • Keep the key out of the loop. Provide the key through an environment variable or secret store the agent does not print; agents should write code that reads it, never embed it. See Authentication and Security.
  • Point the agent at the SDK guide. The SDKs page shows the exact accessor for citations in each SDK and how the SDK exception classes map to the error envelope; agents otherwise tend to assume the field is absent or parse it from the answer text.
  • Have agents respect Retry-After. Agent-written retry loops are where burst amplification comes from. The official SDKs already honor the header; a hand-written client must read it too (Rate limits).
  • Separate the scopes. Give an agent building a public chat widget a chat key and an agent building the ingestion pipeline an ingest key (Scopes); a 403 insufficient_scope then catches a misplaced call immediately.
  • Test against a throwaway conversation and a small upload. Conversation delete is immediate and document delete is synchronous and irreversible (Documents), so an agent can exercise the full loop without leaving state behind.
  • Generated clients are another option: the spec is standard OpenAPI, so any client generator your stack prefers will work for the eCourtDate-specific endpoints.

llms.txt

llms.txt is served at https://docs.chatbots.ecourtdate.com/llms.txt. It follows the llms.txt convention: a title, a one-paragraph summary, a list of key resources (the interactive reference, the OpenAPI spec in JSON and YAML, and the guides), and a "Facts" block that states the base URL, authentication and scopes, the OpenAI-compatible surface, the citations extension, the endpoints, the limits, the error envelope and codes, the response headers, streaming, and webhooks in plain text. Every fact in it is also in the guides, so an agent that has read llms.txt can be sent to the relevant guide for detail.

Use it when:

  • your assistant accepts a URL or a pasted document as context and you want the whole contract in one short file;
  • you are building a retrieval index over documentation and want one stable entry point that links to everything else;
  • you need a quick sanity check that an agent's assumptions (base URL, header name, error shape) match the API.

The file is maintained alongside the guides and updated in the same change as the spec. For anything it does not state, the OpenAPI spec is the source of truth.