Eldric Docs 5.0.177 ← eldric.ai
API

API reference

API Reference · Developers · Public Edge surface · Applies to 5.0.x

This page consolidates the previous API pages into one maintainable reference. It documents only the customer-facing public API reachable through the Edge with API-key or session authentication.

Authentication and access

This page is a curated subset, not an index. It covers the endpoints most callers need; the product serves far more, and an endpoint's absence here is not evidence it does not exist.

Public API calls use an API key or an authenticated session. Tenant scope is enforced by the Edge. Cross-tenant access returns an authorization error even when the path shape is otherwise valid.

From 5.0.168, inference needs a login from outside. Through the public edge, /v1/chat/completions and /v1/embeddings answer 401 without credentials, and so do /health, /metrics, /api/v1/system/version, /api/v1/system/incidents, /api/v1/license/health and /api/v1/cloud/backends/public. Earlier releases answered them anonymously. Calls made inside the cluster and on the host itself are unchanged. If you run your own reverse proxy, it has to mark public requests with X-Eldric-Public-Edge: 1, or these paths stay open. See the 5.0.168 release notes.

⚠ From 5.0.170 (breaking): chat, completions and embeddings always need a login, inside your network too. Once authentication is on, /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/cloud/embeddings and /v1/inference/embeddings answer 401 to any call without an API key or session, wherever it comes from. That includes calls from your LAN or straight to port 8880. The proxy header no longer matters for these paths. /v1/models still answers without a login. Give every integration that calls these paths a key before you update.

HeaderUse
X-API-KeyTenant-scoped API access for integrations.
Content-Type: application/jsonRequired for JSON request bodies.
Authorization: Bearer Session/JWT access where enabled by the deployment.

OpenAI-compatible

POST/v1/chat/completions

An unknown model does not fail. If no backend serves the model you asked for, the request is answered by a different one rather than rejected. Measured 2026-08-20 on two nodes with a made-up id:

X-Eldric-Model-Substitution: totally-made-up-model-xyz->mistral-large-3:675b
X-Eldric-Model-Substitution: totally-made-up-model-xyz->qwen3.5:397b

Two things follow for a client. The response's model field names the model that answered, not the one you requested — so comparing it against your request is how you detect a swap in the body. And the substitute is not fixed: it depends on which node serves the request and on which models that node currently has loaded — the same made-up id returned qwen3.5:397b from one node on 19 August and mistral-large-3:675b from the same node a day later. Do not cache the mapping.

If you need the model you asked for, check the header or the model field before trusting the answer. A 200 is not a confirmation that your model ran. Substitution works the same way with stream: true — the header is sent and every frame's model field names the substitute (verified on two nodes, 2026-08-20).

Streaming errors do not arrive as an error status. When a request fails after the stream has been opened, the HTTP status can be 200 and the failure arrives inside the first frame — sometimes as a JSON string nested in detail, so it needs decoding twice. Measured on two nodes, 2026-08-20, omitting messages:

stream: false -> 400  {"error":{"message":"[] is too short - 'messages' …"}}
stream: true  -> 200  data: {"detail":"{"error":{"message":"[] is too short …"}}"}

Not every error diverges — a missing model returns 400 on both paths. So with stream: true, read the first frame before treating a 200 as success.

GET/v1/models

Return the model catalogue available to the caller. There is no per-model detail endpoint on this API and no legacy /v1/completions; use /v1/chat/completions.

POST/v1/chat/completions

Create a chat completion. Supports streaming and compatible tool fields where enabled.

POST/v1/embeddings

Create embeddings.

POST/api/v1/models/show

4.x only — not served by 5.0. Measured 2026-08-27: registered nowhere in the 5.0 module or kernel trees (present only in the frozen 4.x controller and edge). On a 5.0 cluster this returns 404. For model detail use GET /api/v1/models, or the owner-specific surfaces: GET /api/v1/inference/models/{id} for native inference and GET /api/v1/data/models/{id} for model storage.

Chat and conversations

The conversation API in 5.0 is the one listed here, keyed by session. A path-id conversation API under /api/v1/conversations (branching, sharing a conversation, nested artifacts) was designed but is not served by 5.0, so it is no longer listed. Artifacts are a top-level surface at /api/v1/artifacts, and /api/v1/artifacts/share/ shares an artifact, not a conversation. Checked against the 5.0 source, 2026-09-28.

GET/chat

Open the browser chat shell.

GET/login

Open the login form when session authentication is used.

GET/health

Public health check — reachable without a token, and the quickest confirmation that the daemon is up. It reports the running version. Note the path has no /api/v1 prefix.

Served today — the session-keyed conversation API

Verified against the 5.0 source on 2026-08-20. These are keyed by the caller's session, not by a path id, so they are a different shape from the designed resource API below — not the same endpoints at another prefix.

GET/api/v1/edge/conversations

List the caller's own conversations.

POST/api/v1/edge/conversations

Create one. JSON body optional; title is honoured when present.

PATCH/api/v1/edge/conversations

Rename. JSON body; a malformed body is rejected with 400.

DELETE/api/v1/edge/conversations

Delete the caller's own conversations. Self-scoped by session — this is the "clear my history" action and the Article 17 erasure path.

GET/api/v1/edge/conversations/messages

Messages of one conversation. Requires ?id=; without it the call returns 400.

POST/api/v1/edge/conversations/messages

Append a message. The JSON body carries conversation_id.

GET/api/v1/edge/conversations/search

Flat list of {conversation_id, title, role, snippet, ts}, newest first. No pagination — capped at 50 hits.

GET/api/v1/edge/conversations/export

Export a conversation as markdown or plain JSON.

GET/api/v1/users/me/conversations

The caller's conversations as the controller sees them.

Designed, not yet served

Account, identity and auth

Sign-in is POST /api/v1/auth/login. 5.0 serves no logout, token-refresh or second-factor route, and no /api/v1/me/* or /api/v1/identity/{me,register} group, so those are no longer listed. Caller settings are at GET|PUT /api/v1/users/me/settings. Quotas, tools and models exist only as cluster-wide surfaces (/api/v1/metering/quotas, /api/v1/agent/tools, /api/v1/models), not as per-caller views. Checked against the 5.0 source, 2026-09-28.

DELETE/api/v1/identity/users/{id}

Delete a user record. The path is /api/v1/identity/users/{id}; an earlier version of this page showed /api/v1/identity/{id}, which 5.0 does not serve.

POST/api/v1/auth/login

Create an authenticated session.

Data and uploads

GET/api/v1/data/storage/{tenant}/{path}

Read a tenant-scoped file.

PUT/api/v1/data/storage/{tenant}/{path}

Write a tenant-scoped file.

DELETE/api/v1/data/storage/{tenant}/{path}

Delete a tenant-scoped file.

GET/api/v1/data/storage/{tenant}

List tenant-scoped files.

POST/api/v1/upload/init

Reserve a chunked upload.

POST/api/v1/upload/chunk

Upload one chunk.

POST/api/v1/upload/finalize

Commit an upload.

GET/api/v1/upload/{id}/progress

Read upload progress.

DELETE/api/v1/upload/{id}

Cancel an upload.

Vector and RAG

GET/api/v1/vector/namespaces/{tenant}

List vector namespaces.

POST/api/v1/vector/namespaces/{tenant}

Create a namespace.

DELETE/api/v1/vector/namespaces/{tenant}/{namespace}

Delete a namespace.

GET/api/v1/vector/documents/{tenant}/{namespace}/{document}

Get a document.

PUT/api/v1/vector/documents/{tenant}/{namespace}/{document}

Replace and re-index a document.

DELETE/api/v1/vector/documents/{tenant}/{namespace}/{document}

Delete a document.

POST/api/v1/vector/search

Run semantic search.

POST/api/v1/vector/hybrid-search

Run hybrid lexical and vector search.

POST/api/v1/vector/ingest

Ingest and index content.

Memory, agents and tools

POST/api/v1/memory/store

Store an association for later recall.

POST/api/v1/memory/recall

Recall stored context.

POST/api/v1/memory/forget

Remove one stored entry.

GET/api/v1/agent/sessions

List agent sessions.

POST/api/v1/agent/sessions

Create an agent session.

GET/api/v1/agent/sessions/{id}

Read agent session detail.

DELETE/api/v1/agent/sessions/{id}

Delete an agent session.

POST/api/v1/agent/chat

Run agentic RAG chat.

POST/api/v1/agent/multi

Run a multi-agent task.

POST/api/v1/agent/decompose

Break a query into sub-questions.

GET/api/v1/agent/knowledge-bases

List knowledge bases.

POST/api/v1/agent/knowledge-bases

Create a knowledge base.

POST/api/v1/agent/knowledge-bases/{id}/search

Search a knowledge base.

Media, communication, science, IoT and swarm

5.0 has no streaming speech route and no voice-chat route. /api/v1/stt/stream, /api/v1/tts/stream, /api/v1/voice/chat and /api/v1/video/transcribe belong to the retired 4.x daemons and are no longer listed. These are absences, not renames, so they do not exist under another prefix either. The media module serves POST /api/v1/stt/transcribe and POST /api/v1/media/stt for speech-to-text, POST /api/v1/tts/synthesize and POST /api/v1/media/tts for synthesis, and POST /api/v1/media/video/analyze and POST /api/v1/video/extract-frames for video.

POST/api/v1/stt/transcribe

Transcribe audio.

POST/api/v1/tts/synthesize

Synthesize speech.

GET/api/v1/tts/voices

List voices. An empty voices array is not necessarily “this node has no voices” — when no voice directory is configured or the configured one is missing, the response is still 200 with an empty array, and the reason is in the accompanying models_dir and note fields. Read note before treating the list as authoritative.

GET/api/v1/comm/accounts

List communication accounts.

POST/api/v1/comm/messages

Send a message through an enabled channel.

POST/api/v1/comm/search

Stub — returns no results, by construction. Measured 2026-08-20: the handler answers {"results":[],"count":0,"_stub":true}, and nothing in 5.0 persists a message for it to search — comm messages live in memory only (a capped inbox and an ephemeral outbox), and 4.x’s message store is on disk but not compiled into the daemon. Branch on _stub: an empty results array here does not mean “nothing matched”.

POST/api/v1/comm/calls/start

Start an outbound voice call.

GET/api/v1/science/sources

List enabled science sources.

GET/api/v1/science/sources/categories

List science source categories.

POST/api/v1/science/sources/request

Request source enablement.

GET/api/v1/science/tools

List role-filtered science tool schemas.

POST/api/v1/science/tools/execute

Execute a science tool.

GET/api/v1/{category}/sources

List enabled sources through a category alias.

GET/api/v1/iot/devices

List IoT devices.

POST/api/v1/iot/read

Read a device attribute. Send device_id and variable in the body; the device is not part of the path.

POST/api/v1/iot/write

Write a device attribute. Send device_id, variable and value in the body.

GET/api/v1/swarm/swarms

List swarms. The bare plural /api/v1/swarms is not a route and answers 404 — this page said otherwise until 2026-08-23.

POST/api/v1/swarm/swarms

Create a swarm.

POST/api/v1/swarm/goals

Create a goal (plan → approve → execute); GET the same path to list. There is no /api/v1/swarms/{id}/goal.

GET/api/v1/tenants/{tenantId}/theme

Read tenant theme.

GET/api/v1/tenants/{tenantId}/branding/logo

Read tenant logo.

Endpoint detail pattern

Each endpoint should stay maintainable by following the same reference shape. This answers the annotated concern directly: the template is not placeholder UI; it is the required structure for real endpoint entries.

POST /example/path

Purpose
When to use this endpoint and which user or system action it supports.
Auth and permission
Required API key/session, tenant scope and role/capability expectation.
Parameters
Path, query and body fields with type, required state and short notes.
Request example
Copyable curl and JSON example using placeholder host and key variables.
Response
Status codes, response schema summary and a compact example.
Errors
Common failure cases and sanitized public meanings.
Notes
Tenant, version and compatibility notes without internal implementation details.

Errors

StatusMeaning
400Request shape or validation failed.
401No credentials, or a credential this node refused. A refused key or token, from 5.0.175 on /api/v1/*: 401 with WWW-Authenticate: Bearer, error: "invalid_credential" and a reason: api_key_revoked (rotated or revoked, or its user was deleted), api_key_unknown (never issued by this cluster, or incomplete), or bad_encoding, bad_signature, expired, unknown_key for a capability token that does not verify. On /v1/* an invalid key has answered 401 with code invalid_api_key since 5.0.170, as OpenAI and Anthropic SDKs expect. Up to 5.0.174 a refused credential on /api/v1/* answered 403.
403Authenticated, but not allowed in this tenant or role. Up to 5.0.174 also a refused credential on /api/v1/* (and on /v1/* up to 5.0.169).
404Two different things: this resource does not exist, and no route is registered here. Measured 2026-08-23, and the answer is neither “read the code” nor “read the sentence” alone — each carries one half.

Route-missing vs resource-missing: read detail. no handler is registered for this path means no route, and it says that at every site that means it. The code cannot tell you: across the five responses that carry this sentence it is not_found on three and route_not_found on two, while not_found is used at 272 places overall, mostly for a genuinely missing resource. So do not branch on route_not_found alone. The local-only namespaces — /api/v1/auth/*, /api/v1/identity/*, /api/v1/cluster/* and their siblings — answer a missing route with plain not_found, so a client keyed to route_not_found is silent on exactly the paths most likely to be typed wrong, and silent looks like working.

Which layer answered: read the code. route_not_found means the controller's module dispatch handled the request and found no route — it always arrives with method and module beside it, which is the sturdiest anchor a client has. Plain not_found on a route-missing answer means the gateway or the local-only fallback replied instead. Both mean “no route”; only the first tells you a module was consulted. What the code cannot do is separate route-missing from resource-missing — that is the sentence's job, above.

Wrong verb vs nothing-serves-this-path: read the fields. When the body carries module and method alongside the path, that module is loaded here and simply registers no route for that verb — a wrong method, not a missing feature, and reason says so. Without those fields, nothing serves the path at all.

Two body shapes exist: the flat {error, detail, path} and, on OpenAI-compatible paths, {error:{message,type,code}} with the sentence repeated in a top-level detail. Parse for both. And note the sibling case that is not a 404 at all: if the path names a module that no node reports, you get 503 module_unavailable with module and reason — “no peer kernel reports this module — peer-sync may be lagging or no node hosts it”. That is a cluster-state answer, not a routing one: the same request may succeed later without anything changing on your side, so retry before treating it as a missing feature. And note this is a 404 rather than a 405 by deliberate choice: a route that was never written is treated as missing, not as a method restriction on an existing one.
413The request body is larger than this server accepts — the cap is 128 MB for a single request. The body carries error: "payload_too_large". This is a size limit, not a quota: raising it is not the fix. Send files through the chunked upload endpoints (/api/v1/upload/init then /api/v1/upload/chunk), which exist for exactly this case.
405The path exists but does not serve that method. Read the detail before retrying: some paths serve no method in 5.0 — a bare knowledge-base id, for example, where 4.x served GET and DELETE that were never ported. In those cases trying another verb will not help, and the response says so.
429Rate limit or quota exceeded.
500The request failed inside the node. The body carries error: "internal" and a deliberately generic detail: the cluster API keeps fault specifics in the node’s log rather than the response, so there is nothing here to branch on. Treat a 500 as “retry, then escalate with the timestamp” — an operator with log access can tell you what it was.
503Public Edge cannot serve the requested capability. From 5.0.175, credential_check_unavailable means your key could not be checked because no controller was reachable: it was not judged, so keep it and retry.

Every error body carries an error field holding a short machine-readable code, and a detail field holding a human-readable sentence. Match on error; treat detail as text for logs and operators, not as a value to branch on. A few rejections happen at the protocol layer, before any handler runs — an oversized body, an unparseable request, an unsupported method or HTTP version. Those carry the same error and detail fields plus a numeric status, so one parser handles both kinds.

{"error":"not_found","detail":"no handler is registered for this path","path":"/v1/completions"}

On /api/v1/*, a rejected call tells you nothing about whether the path exists. Authentication is checked before the route is looked up, so a call without a key returns 401 — and so does a call with a key the node does not accept (403 up to 5.0.174) — whether or not the endpoint is served. These answers look identical for a real endpoint and for a path that was never registered. Only a call the node accepts distinguishes them, so never infer that an endpoint is missing from a 401 or a 403.

What the answer does tell you is whether a credential was presented. From 5.0.175: a 401 with error: "invalid_credential" means a credential arrived and this node refused it, and its reason names why; a 401 without it means none arrived. Up to 5.0.174: 401 meant none arrived, and 403 meant one arrived and was refused, with a reason. Every 401 carries WWW-Authenticate. So a refusal is a signal to look at your key, and a missing credential is a signal that it never got sent — a proxy stripping the header, or the wrong header name.

reason depends on the shape of what you sent, and only then on the node. A credential that is not a capability token at all reads as bad_encoding everywhere, consistently. A correctly-shaped token can read as bad_encoding on one node and unknown_key on another.

unknown_key means the token is well formed but the key named in it could not be found. The most common cause is that the tenant it was issued for no longer exists — a terminated tenant's key is dropped, and tokens signed with it then fail this way rather than as a bad signature, so that a terminated tenant can be told apart from a tampered token. Check the tenant before you check the node.

And do not conclude a token is malformed from a single node. If a capability-shaped token is refused as bad_encoding, try another node before assuming the token is broken.