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.
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.
Header
Use
X-API-Key
Tenant-scoped API access for integrations.
Content-Type: application/json
Required 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:
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.
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
Status
Meaning
400
Request shape or validation failed.
401
No 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.
403
Authenticated, 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).
404
Two 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 503module_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.
413
The 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.
405
The 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.
429
Rate limit or quota exceeded.
500
The 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.
503
Public 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.