Skip to Content
DocumentationAPI Reference

API Reference

Eneo exposes a REST API (FastAPI) that the web app itself runs on. This page describes the API contract: where the API lives, how to authenticate, how the chat endpoint streams, and the pagination, error, and rate-limit conventions that apply everywhere. The endpoint catalogue is the OpenAPI specification served by your own instance, not this page.


Base URL and OpenAPI specification

All versioned endpoints are mounted under the backend’s API_PREFIX, which is /api/v1 in the shipped configuration templates. A full URL therefore looks like https://your-domain.com/api/v1/spaces/.

The interactive documentation and the specification are served at the root of the backend, not under the prefix:

ResourceProduction (behind the reference Traefik setup)Local development
Swagger UIhttps://your-domain.com/docshttp://localhost:8123/docs
OpenAPI JSONhttps://your-domain.com/openapi.jsonhttp://localhost:8123/openapi.json
ReDocnot routedhttp://localhost:8123/redoc
Backend versionhttps://your-domain.com/versionhttp://localhost:8123/version

The reference docker-compose.yml routes only /api, /scim, /docs, /openapi.json, and /version to the backend, so /redoc is available in local development only. GET /version returns {"version": "..."} and the OpenAPI document carries the same version string, so the spec you download always matches the running backend.

Paths are exact. Most routers use a trailing slash (/api/v1/users/me/, /api/v1/conversations/), while the API key endpoints do not (/api/v1/api-keys). Copy paths from the specification rather than guessing.

Health probes live outside the versioned prefix. GET /api/healthz requires no authentication and returns 200 or 503 with backend, worker, and object-store status. /api/livez and /api/readyz are also public and exist for orchestrators, but are hidden from the specification.

GET /api/healthz/crawler returns detailed crawler diagnostics and requires the deployment’s ENEO_SUPER_API_KEY, sent in X-API-Key (or the configured API_KEY_HEADER_NAME). User sessions and ordinary tenant API keys do not grant access. A missing or invalid super API key returns 401. Use the public health probes for orchestrator checks.

The specification includes the Server-Sent Event payload models described below under components.schemas, and names its API key security scheme APIKeyAuth.


Authentication

Every endpoint under /api/v1 accepts either an API key or a session token. Some operations are restricted to one of them; see Session-only and user-only operations.

API keys

Send the key in the X-API-Key header. The header name is configurable through the backend’s API_KEY_HEADER_NAME setting; X-API-Key is the template default.

curl "https://your-domain.com/api/v1/spaces/" \ -H "X-API-Key: sk_..."

Two key types exist: sk_ secret keys for server-to-server use and pk_ public keys for browser integrations. Keys are created in the web app under Account → API keys (/account/api-keys); tenant administrators manage every key in the tenant under Admin → API keys (/admin/api-keys). The account page has a Create API Key button. Keys can also be created with POST /api/v1/api-keys, which requires a session token and the api_keys role permission.

An API key carries a scope (tenant, space, assistant, or app), a permission level, and optional guardrails. By default the HTTP method decides the required permission level: GET, HEAD, and OPTIONS need read; POST, PUT, and PATCH need write; DELETE needs admin. A few semantically read-only POST endpoints, including the chat endpoint, accept read. Scopes, permission levels, guardrails, ownership, and lifecycle are covered in API Key Management.

Session tokens

Users who sign in with email and password obtain a JWT from the OAuth2 password-flow endpoint. The request body is form-encoded, not JSON:

curl -X POST "https://your-domain.com/api/v1/users/login/token/" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "username=user@example.com" \ --data-urlencode "password=..."
{ "access_token": "eyJhbGc...", "token_type": "bearer" }

Send the token as Authorization: Bearer <access_token>. Invalid credentials return 401.

Failed attempts are limited to 5 per login name within 10 minutes. Each 401 reports how many remain, and once none remain every further attempt for that name returns 429 until the window ends, including one with the correct password. A successful login restores the full allowance. The limit counts the submitted name whether or not an account exists, so it does not reveal which names are registered.

{ "code": "invalid_credentials", "message": "Invalid credentials", "attempts_remaining": 3 }
{ "code": "too_many_login_attempts", "message": "Too many failed login attempts. Try again later.", "attempts_remaining": 0, "retry_after_seconds": 412 }

The 429 response also carries a Retry-After header. The limit applies to password login only; API keys and OIDC sign-in are unaffected.

Users of tenants that authenticate through OIDC federation obtain their token from the /api/v1/auth/* flow instead; see Multi-Tenant OIDC Federation. Both this flow and POST /api/v1/users/login/openid-connect/mobilityguard/ return 403 when a validated identity is not permitted to access or join the configured tenant. A missing account requires SCIM/admin provisioning or all JIT admission requirements.

If a request carries both an Authorization: Bearer token and an API key header, the bearer token is used and the API key is ignored.

Session-only and user-only operations

The backend applies three guards on top of ordinary authentication. Each returns 403 with a stable code:

GuardRejectscodeApplies to
Session requiredevery API key, including tenant-admin service keyssession_auth_requiredAll API key mutations (POST /api/v1/api-keys, update, delete, revoke, rotate, extend, purge, suspend, reactivate, notification preferences), chat-turn diagnostics, Skills, module administration and the module login handoff, object-store connection and deployment-policy administration, and the logging endpoints behind Admin Insights
User identity requiredservice keys (ownership: "service")user_identity_requiredEndpoints scoped to the caller as a person, such as GET /api/v1/users/me/
User required for creationservice keysservice_key_cannot_create_resourcesPOST endpoints that create user-owned resources (assistants, apps, services, spaces, group chats, collections, websites, files, info blobs, roles, MCP servers, help-assistant runs, and API keys)

Session tokens pass all three guards; user-owned API keys pass the second and third. Scoped keys are additionally checked against the resource they target and receive 403 with code: "insufficient_scope" when it lies outside the key’s scope.


Chat

A single endpoint starts or continues a conversation with an assistant or a group chat:

POST /api/v1/conversations/

Request body

FieldTypeNotes
questionstringRequired.
session_idUUIDContinue an existing conversation.
assistant_idUUIDStart a new conversation with an assistant.
group_chat_idUUIDStart a new conversation with a group chat.
streambooleanDefault false. true returns Server-Sent Events.
fileslist of {"id": UUID}Previously uploaded files to attach.
toolsobject{"assistants": [{"id": UUID, "handle": string}]} targets a specific assistant (mentions in group chats, tool assistants otherwise).
use_web_searchbooleanDefault false.
require_tool_approvalbooleanDefault false. Requires stream: true; not supported for group chats.
disabled_mcp_server_idslist of UUIDMCP servers to switch off for this message. It can only narrow the active set.

Exactly one of session_id, assistant_id, or group_chat_id must be present; anything else fails validation with 422. The optional query parameter version (1 or 2, default 1) selects the knowledge-retrieval behaviour; the web app sends version=2.

Sending require_tool_approval: true without stream: true returns 400 with code: "invalid_request"; sending it for a group chat returns 400 with code: "not_supported".

Non-streaming response

With stream: false the endpoint returns one JSON object once the answer is complete. Its fields are id, session_id, question, answer, files, generated_files, references, tools, web_search_references, mcp_tool_references, completion_model, created_at, and updated_at. The exact schema is AskChatResponse in the specification.

curl -X POST "https://your-domain.com/api/v1/conversations/" \ -H "X-API-Key: sk_..." \ -H "Content-Type: application/json" \ -d '{"assistant_id": "ASSISTANT_UUID", "question": "Summarise the travel policy."}'

Streaming response (Server-Sent Events)

With stream: true the same POST request responds with Content-Type: text/event-stream. There is no separate GET stream endpoint, so the browser EventSource API cannot be used; read the response body of the POST instead. The server sends a comment line every 15 seconds to keep the connection alive.

Each event is a standard SSE frame whose event field names the payload type and whose data field is one JSON document. Every payload carries session_id.

eventPayload modelContent
first_chunkSSEFirstChunkEmitted first. Same shape as the non-streaming response with an empty answer; carries the session_id to continue the conversation.
textSSETextanswer (a text delta) and references (knowledge chunks used). Concatenate the deltas to form the full answer.
reasoningSSEReasoningreasoning, a chunk of the model’s thinking text.
imageSSEFilesgenerated_files, files produced by the model.
eneo_eventSSEEneoEventeneo_event_type, currently generating_image.
tool_callSSEToolCalltools being executed (server, tool name, arguments, tool_call_id, status) and mcp_tool_references. Tool results are omitted from the stream; fetch them with GET /api/v1/conversations/{session_id}/tool-calls/{tool_call_id}/result/.
tool_approval_requiredSSEToolApprovalRequiredapproval_id and the pending tools. Only with require_tool_approval: true.
tool_approval_timeoutSSEToolApprovalTimeoutapproval_id and the tools whose approval expired.
token_usageSSETokenUsageusage with prompt, completion, turn, and context token counts.
errorSSEErrorerror (message) and optional numeric error_code.
curl -N -X POST "https://your-domain.com/api/v1/conversations/" \ -H "X-API-Key: sk_..." \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ -d '{"assistant_id": "ASSISTANT_UUID", "question": "Summarise the travel policy.", "stream": true}'
event: first_chunk data: {"session_id": "5f1c...", "question": "Summarise the travel policy.", "answer": "", ...} event: text data: {"session_id": "5f1c...", "answer": "Travel must", "references": []} event: text data: {"session_id": "5f1c...", "answer": " be approved", "references": []} event: token_usage data: {"session_id": "5f1c...", "eneo_event_type": "token_usage", "usage": {...}}

When a tool_approval_required event arrives, submit decisions with POST /api/v1/conversations/approve-tools/?approval_id=<approval_id> and a JSON body that is a list of {"tool_call_id": string, "approved": boolean, "reason"?: string}. Omitted tool calls are treated as rejected.

JavaScript client

The repository ships a typed client, @eneo/eneo-js (frontend/packages/eneo-js), that the web app consumes as a workspace package. It sends the API key in X-API-Key (configurable via apiKeyHeaderName), a session token as Authorization: Bearer, or both, and wraps the streaming protocol above:

import { createEneo } from "@eneo/eneo-js"; const eneo = createEneo({ apiKey: process.env.ENEO_API_KEY, baseUrl: "https://your-domain.com", }); const conversation = await eneo.conversations.ask({ chatPartner: { type: "assistant", id: "ASSISTANT_UUID" }, question: "Summarise the travel policy.", files: [], callbacks: { onText: (chunk) => process.stdout.write(chunk.answer), onToolCall: (event) => console.log(event.tools), }, }); // Resolves with the accumulated answer once the stream closes. console.log(conversation.session_id, conversation.answer);

ask always streams; pass conversation: { id } instead of chatPartner to continue a session. For endpoints without a wrapper, eneo.client.fetch("/api/v1/...", { method, params, requestBody }) is typed against the generated OpenAPI schema. Every call may throw an EneoError carrying the failing request and the parsed error body.

Other conversation endpoints

  • GET /api/v1/conversations/?assistant_id=... or ?group_chat_id=... lists conversations (cursor-paginated, see below). Exactly one of the two filters is required.
  • GET /api/v1/conversations/{session_id}/ returns a conversation with its messages.
  • DELETE /api/v1/conversations/{session_id}/ deletes it (204).
  • PATCH /api/v1/conversations/{session_id}/name/ renames it with {"name": "..."}.
  • POST /api/v1/conversations/{session_id}/title/ generates a title with the model.
  • POST /api/v1/conversations/{session_id}/feedback/ records feedback.
  • POST /api/v1/conversations/preflight estimates the token cost of the next message before sending it.

Pagination

Two response shapes are used.

Plain lists return PaginatedResponse: items plus count, the number of items in the response. GET /api/v1/spaces/ is an example; it accepts the boolean query parameters include_applications and include_personal and returns every space the caller can access in one page.

{ "items": [{ "id": "...", "name": "Marketing" }], "count": 1 }

Cursor-paginated lists return CursorPaginatedResponse, which adds limit, next_cursor, previous_cursor, and total_count. Request pages with the query parameters limit, cursor, and previous (true to page backwards). GET /api/v1/conversations/ uses a timestamp cursor; GET /api/v1/api-keys uses an opaque string cursor. Always pass the cursor value back verbatim.

{ "items": [{ "id": "...", "name": "Travel policy" }], "count": 1, "limit": 20, "next_cursor": "2026-09-01T10:30:00Z", "previous_cursor": null, "total_count": 42 }

Errors

Errors are JSON. Four shapes occur, depending on where the error is raised:

ShapeWhen
{"detail": "message"}Simple HTTP errors, and authentication failures without a code.
{"detail": [{"loc": [...], "msg": "...", "type": "..."}]}422 request validation errors (FastAPI default).
{"code": "...", "message": "...", "request_id"?: "...", "context"?: {...}}Structured errors from API key authentication, scope checks, and endpoints that raise coded errors. code is stable; message is not.
{"message": "...", "eneo_error_code": ..., "code"?: "...", "request_id"?: "...", "context"?: {...}, "details"?: {...}}Domain errors mapped by the global exception handlers (not found, unauthorized, conflicts, and so on).

Unhandled failures return 500 with {"error": "Internal server error", "error_id": "...", "message": "..."}. Quote the error_id when reporting a problem; in development mode the body also contains a detail object with the exception type and path.

Every response carries an X-Trace-Id header (and X-Correlation-ID for older clients). If you send X-Correlation-ID or X-Request-ID with a request, the value is echoed as request_id in structured error bodies, which makes client and server logs easy to correlate.

Handle errors by code or eneo_error_code, never by parsing the English message.

StatusTypical meaning
400Invalid request, including bad target combinations and policy violations
401Missing, invalid, expired, revoked, or suspended credentials
403Authenticated but not permitted: scope, permission, ownership, or guard failures
404Resource not found or not visible to the caller
409Conflict, for example a file still referenced by a conversation
422Request body or query parameters failed validation
429Rate limit exceeded
503A dependency such as Redis or the worker is unavailable

Rate limiting

Rate limiting applies to API key traffic only; session-token requests are not globally limited. A few endpoints enforce their own per-user limits regardless of credential type, such as chat preflight (600 requests per minute) and tool approval (20 per minute).

Each API key gets a fixed window of API_KEY_RATE_LIMIT_WINDOW_SECONDS (default 3600 seconds, one hour) with a default budget by scope:

ScopeDefault requests per windowSetting
Tenant10 000API_KEY_RATE_LIMIT_TENANT_DEFAULT
Space5 000API_KEY_RATE_LIMIT_SPACE_DEFAULT
Assistant1 000API_KEY_RATE_LIMIT_ASSISTANT_DEFAULT
App1 000API_KEY_RATE_LIMIT_APP_DEFAULT

A key’s own rate_limit field overrides the default; -1 disables the limit for that key (tenant administrators only, and not when the tenant policy sets max_rate_limit_override). See API Key Management.

Rate-limit headers are sent only on a 429 response; successful responses carry none:

HTTP/1.1 429 Too Many Requests Retry-After: 3600 X-RateLimit-Limit: 5000 X-RateLimit-Remaining: 0 {"code": "rate_limit_exceeded", "message": "API key rate limit exceeded."}

If Redis is unavailable the backend fails closed with 503 and code: "rate_limit_unavailable", unless the operator has enabled API_KEY_RATE_LIMIT_FAIL_OPEN.