API Reference
Selu exposes two groups of HTTP endpoints: the v1 conversation API (used by both the web app and native mobile clients) and admin-only management endpoints for agents, policies, and system updates. All endpoints require authentication via session cookie.
v1 Conversation API
Section titled “v1 Conversation API”The v1 API is the shared contract between the Selu web app and iOS. It provides conversation management, message sending, a replayable event feed, and tool-approval decisions.
GET /api/v1/session
Section titled “GET /api/v1/session”Returns basic information about the signed-in user. The web app uses this to render role-appropriate navigation without storing credentials in the browser.
Response:
{ "display_name": "Ada Lovelace", "is_admin": false, "language": "en", "supports_photo_uploads": true}| Field | Type | Description |
|---|---|---|
display_name | string | User’s display name |
is_admin | boolean | Whether the user has admin privileges |
language | string | User’s preferred language code (e.g. "en", "de") |
supports_photo_uploads | boolean | Whether this server accepts photo attachments on messages |
GET /api/v1/conversations
Section titled “GET /api/v1/conversations”List conversations for the signed-in user, ordered by most recent activity first.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer | no | Number of conversations to return (1–100, default 50) |
before | string | no | Opaque cursor from next_cursor in a previous response |
Response:
{ "conversations": [ { "id": "01J...", "channel_id": "pipe-abc", "channel_name": "My Web Chat", "title": "Weekend trip ideas", "status": "active", "kind": "regular", "created_at": "2026-09-03T10:00:00.000Z", "last_activity_at": "2026-09-03T10:15:00.000Z", "active_run_id": null } ], "next_cursor": "2026-09-03T10:00:00.000Z|01J..."}Response fields:
| Field | Type | Description |
|---|---|---|
conversations | array | Page of conversation summaries |
next_cursor | string | omitted | Pass back as ?before= to fetch the next (older) page. Omitted when no older page exists. |
ConversationSummary fields:
| Field | Type | Description |
|---|---|---|
id | string | Unique conversation ID |
channel_id | string | ID of the pipe this conversation belongs to |
channel_name | string | Human-readable pipe name |
title | string | null | User-visible title, null if not yet set |
status | string | "active" or "archived" |
kind | string | "regular" for user-started conversations, "schedule" for schedule-generated threads |
created_at | string | ISO 8601 timestamp |
last_activity_at | string | ISO 8601 timestamp of the most recent message |
active_run_id | string | null | ID of an in-progress run, or null |
POST /api/v1/conversations
Section titled “POST /api/v1/conversations”Create a new conversation.
Response: A ConversationSummary object (see above).
GET /api/v1/conversations/{conversation_id}
Section titled “GET /api/v1/conversations/{conversation_id}”Return a full snapshot of the conversation: its metadata, messages, and recent runs. The snapshot also includes an event_cursor you should pass to GET /api/v1/events to receive only events that happened after this snapshot was taken.
Response:
{ "conversation": { "...ConversationSummary fields..." }, "messages": [ { "id": "msg-001", "role": "user", "content": "What's the weather like?", "created_at": "2026-09-03T10:00:00.000Z", "tool_calls": null, "tool_call_id": null, "attachments": null, "compacted": false } ], "runs": [ { "id": "run-001", "client_message_id": "client-uuid", "status": "completed", "error_code": null, "created_at": "2026-09-03T10:00:00.000Z", "started_at": "2026-09-03T10:00:01.000Z", "completed_at": "2026-09-03T10:00:05.000Z" } ], "event_cursor": 42}ConversationMessage fields:
| Field | Type | Description |
|---|---|---|
id | string | Message ID |
role | string | "user", "assistant", "tool", or "status" |
content | string | Text content of the message |
created_at | string | ISO 8601 timestamp |
tool_calls | object | null | Tool call details when role is "assistant" and tools were invoked |
tool_call_id | string | null | Present on "tool" role messages |
attachments | object | null | Attached files or images |
compacted | boolean | Whether this message was compacted during context window management |
ConversationRun fields:
| Field | Type | Description |
|---|---|---|
id | string | Run ID |
client_message_id | string | Idempotency key supplied by the client |
status | string | See run statuses below |
error_code | string | null | Machine-readable error code if the run failed |
created_at | string | ISO 8601 timestamp |
started_at | string | null | When the run started processing |
completed_at | string | null | When the run finished |
Run statuses:
| Status | Meaning |
|---|---|
queued | Run is waiting to start |
running | Agent is actively processing |
waiting_for_approval | Paused — waiting for the user to approve a tool call |
cancelling | Cancellation requested, still winding down |
completed | Finished successfully |
failed | Finished with an error |
cancelled | Cancelled before completion |
interrupted | Stopped unexpectedly |
PATCH /api/v1/conversations/{conversation_id}
Section titled “PATCH /api/v1/conversations/{conversation_id}”Rename a conversation. The new title is user-facing only — the agent never reads it.
Request body:
{ "title": "Weekend trip ideas"}| Field | Type | Required | Constraints |
|---|---|---|---|
title | string | yes | Non-empty, max 120 characters |
Response: The updated ConversationSummary.
Error responses:
| Status | Code | Meaning |
|---|---|---|
| 400 | conversation.invalid_title | Title was empty or missing |
| 404 | — | Conversation not found or not owned by you |
DELETE /api/v1/conversations/{conversation_id}
Section titled “DELETE /api/v1/conversations/{conversation_id}”Delete a conversation together with all its messages, runs, events, and stored artifacts.
Error responses:
| Status | Code | Meaning |
|---|---|---|
| 404 | — | Conversation not found or not owned by you |
| 409 | conversation.run_in_progress | A run is currently active — wait for it to finish before deleting |
Response: HTTP 204 on success.
POST /api/v1/conversations/{conversation_id}/messages
Section titled “POST /api/v1/conversations/{conversation_id}/messages”Send text, photos, or both and start an agent run. The endpoint is idempotent: retry the same logical message with the same client_message_id to receive the existing run instead of creating a duplicate. The request body limit is 18 MiB.
Request body:
{ "text": "What's the weather like?", "client_message_id": "550e8400-e29b-41d4-a716-446655440000", "attachments": [ { "filename": "photo.jpg", "mime_type": "image/jpeg", "data_base64": "<base64-encoded image data>" } ]}| Field | Type | Required | Description |
|---|---|---|---|
text | string | unless photos are present | Message text; may be empty for a photo-only message |
client_message_id | string | yes | Stable client-generated UUID; reuse it when retrying unchanged content |
attachments | array | no | Up to 10 photo attachments |
Attachment fields:
| Field | Type | Description |
|---|---|---|
filename | string | Original filename |
mime_type | string | image/jpeg, image/png, image/gif, or image/webp |
data_base64 | string | Base64-encoded image bytes |
Limits:
| Limit | Value |
|---|---|
| Photos per message | 10 |
| Per-photo decoded size | 2 MiB |
| Total decoded size | 12 MiB |
| JSON request body | 18 MiB |
The server verifies the file signature rather than trusting only mime_type.
| Error code | Meaning |
|---|---|
conversation.invalid_photo | The signature is not a supported image type |
conversation.photo_too_large | A photo exceeds 2 MiB |
conversation.too_many_photos | More than 10 photos were attached |
conversation.photo_command | Photos were combined with a slash command |
Response: The created ConversationRun.
POST /api/v1/conversations/{conversation_id}/feedback
Section titled “POST /api/v1/conversations/{conversation_id}/feedback”Rate the latest completed reply in a conversation.
Request body:
{ "rating": 1 }| Value | Meaning |
|---|---|
1 | Helpful |
-1 | Not helpful |
The rating is stored on the most recent turn signal and feeds the agent’s behavioral-lessons pipeline.
Responses:
| Status | Code | Meaning |
|---|---|---|
204 | — | Rating saved |
400 | conversation.invalid_feedback | rating was not 1 or -1 |
404 | conversation.feedback_unavailable | No completed reply is available to rate |
409 | conversation.run_in_progress | A run is still active; wait for it to finish |
The conversation snapshot exposes the saved value as latest_turn_rating (1, -1, or null), so clients can restore the selected state after a reload.
GET /api/v1/commands
Section titled “GET /api/v1/commands”Return the localized slash-command catalog so clients can render command pickers without requiring a client release when commands change.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
lang | string | no | en or de; defaults to the signed-in user’s language |
Response:
{ "commands": [ { "command": "/remind ", "label": "/remind", "description": "Set a one-time reminder", "argument_hint": "what to do + when" }, { "command": "/help", "label": "/help", "description": "Show the available commands", "argument_hint": null } ]}| Field | Type | Description |
|---|---|---|
command | string | Text inserted into the composer, including a trailing space when an argument is required |
label | string | Short form displayed in the picker |
description | string | Localized one-line description |
argument_hint | string | null | Localized argument placeholder, when applicable |
GET /api/v1/events
Section titled “GET /api/v1/events”An authenticated Server-Sent Events stream that delivers live conversation updates to the client.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
cursor | integer | yes | Last event ID the client has seen. Use the event_cursor from the conversation snapshot. |
The server replays any events with an ID greater than cursor immediately on connect, then streams new events as they happen. This makes the feed reconnect-safe: if your connection drops, reconnect with the last event ID you received and you won’t miss anything.
Event envelope:
{ "id": 43, "conversation_id": "01J...", "run_id": "run-001", "type": "message.delta", "entity_id": "msg-002", "payload": { "delta": "sunny" }, "created_at": "2026-09-03T10:00:03.000Z"}| Field | Type | Description |
|---|---|---|
id | integer | Monotonically increasing event ID. Use this as your next cursor. |
conversation_id | string | Which conversation this event belongs to |
run_id | string | null | The run that produced this event, if applicable |
type | string | Event type (see table below) |
entity_id | string | null | ID of the affected entity (message ID, run ID, etc.) |
payload | object | Event-specific data |
created_at | string | ISO 8601 timestamp |
Event types:
| Type | Payload | Description |
|---|---|---|
message.created | { id, role, content, ... } | A new message was added to the conversation |
message.delta | { delta: string } | Streaming text chunk for an in-progress assistant message |
run.started | { run_id } | An agent run began |
run.tool_call | { name, arguments, ... } | The agent called a tool |
run.tool_result | { tool_call_id, content } | A tool returned a result |
run.approval_required | { approval_id, tool_name, ... } | The run is paused waiting for your approval |
run.output_finished | { run_id } | The agent’s reply is complete |
run.completed | { run_id, status } | The run reached a terminal state |
conversation.changed | { title } | Conversation metadata was updated (e.g. renamed) |
conversation.deleted | {} | The conversation was deleted (transient — not persisted) |
GET /api/v1/artifacts/{artifact_id}
Section titled “GET /api/v1/artifacts/{artifact_id}”Download an artifact using the active session. Access is owner-scoped: requesting another user’s in-memory or disk-backed artifact returns 404.
The response contains the raw file with appropriate Content-Type and Content-Disposition headers. Images are served inline; other files are served as attachment.
POST /api/v1/approvals/{approval_id}/decision
Section titled “POST /api/v1/approvals/{approval_id}/decision”Approve or deny a pending tool call that is waiting for your confirmation.
Approval IDs are scoped to their owner. Attempting to decide an approval you don’t own returns 404 without consuming the pending approval.
Request body:
{ "approved": true}| Field | Type | Required | Description |
|---|---|---|---|
approved | boolean | yes | true to allow the tool call, false to block it |
Response: HTTP 200 on success.
Admin endpoints
Section titled “Admin endpoints”The following endpoints require the signed-in user to have is_admin = true. Non-admin requests return HTTP 403.
Agent management
Section titled “Agent management”POST /agents/update/start
Section titled “POST /agents/update/start”Start an agent update in the background.
Request:
POST /agents/update/startContent-Type: application/x-www-form-urlencoded
entry_json={"id":"weather","name":"Weather Assistant",...}| Parameter | Type | Description |
|---|---|---|
entry_json | string | JSON-encoded marketplace entry for the agent to update |
Response:
{ "job_id": "550e8400-e29b-41d4-a716-446655440000" }GET /agents/update/status/{job_id}
Section titled “GET /agents/update/status/{job_id}”Poll the status of a background agent update job.
Response:
{ "job_id": "550e8400-e29b-41d4-a716-446655440000", "agent_id": "weather", "agent_name": "Weather Assistant", "target_version": "1.2.0", "progress": 75, "message_key": "agents.update.phase.downloading", "done": false, "success": false, "redirect_to": null, "error_key": null}| Field | Type | Description |
|---|---|---|
progress | number | Completion percentage (0–100) |
message_key | string | i18n key for the current status message |
done | boolean | Whether the job has finished |
success | boolean | Whether it finished successfully (only meaningful when done is true) |
redirect_to | string | null | URL to navigate to after completion |
error_key | string | null | i18n key for the error message if the job failed |
Status message keys:
| Key | Meaning |
|---|---|
agents.update.phase.preparing | Job is starting |
agents.update.phase.downloading | Downloading Docker images |
agents.update.success.updated | Update completed |
agents.update.success.setup_required | Update completed, setup required |
agents.update.error.docker | Docker connection failed |
agents.update.error.start | Could not start the update |
agents.update.error.failed | Update process failed |
POST /agents/update (legacy)
Section titled “POST /agents/update (legacy)”Synchronous update endpoint. Still available for form-based flows but may block for several minutes.
Response: HTTP 302 redirect to the agents page or setup wizard.
Tool policy management
Section titled “Tool policy management”POST /agents/{agent_id}/policy
Section titled “POST /agents/{agent_id}/policy”Update a tool policy for a specific agent.
| Parameter | Location | Description |
|---|---|---|
agent_id | path | Agent identifier |
capability_id | form | Capability ID; use __builtin__ for built-in tools |
tool_name | form | Tool name |
policy | form | allow, ask, or block |
scope | form | global (admin only) or user |
Response: HTTP 200 (empty body) on success.
POST /agents/{agent_id}/policy/reset
Section titled “POST /agents/{agent_id}/policy/reset”Reset a user’s personal policy override back to the global default.
| Parameter | Location | Description |
|---|---|---|
agent_id | path | Agent identifier |
capability_id | form | Capability ID |
tool_name | form | Tool name |
return_to | form | Optional URL to redirect to after reset |
Response: HTTP 200 with HX-Redirect header pointing to the agent permissions page.
System updates
Section titled “System updates”GET /system-updates
Section titled “GET /system-updates”Display the system update management page.
POST /system-updates/channel
Section titled “POST /system-updates/channel”Change the release channel.
| Parameter | Type | Values |
|---|---|---|
release_channel | string | stable, dev, or nightly |
Response: HTTP 302 redirect.
POST /system-updates/check
Section titled “POST /system-updates/check”Manually check for available updates.
POST /system-updates/apply
Section titled “POST /system-updates/apply”Apply an available update.
POST /system-updates/rollback
Section titled “POST /system-updates/rollback”Roll back to the previous version.
GET /system-updates/job-status
Section titled “GET /system-updates/job-status”Returns the current system update status for live UI polling.
Response:
{ "active_job_id": "550e8400-e29b-41d4-a716-446655440000", "status": "updating", "progress_key": "updates.progress.downloading", "installed_version": "v1.5.0", "installed_display": "v1.5.0 (build 123)", "available_version": "v1.5.1", "available_display": "v1.5.1 (build 124)", "available_changelog_url": "https://selu.bot/changelog/v1.5.1", "update_available": true, "rollback_available": true}Progress keys:
| Key | Meaning |
|---|---|
updates.progress.checking | Fetching release metadata |
updates.progress.downloading | Downloading the new Docker image |
updates.progress.restarting | Restarting with the new version |
updates.progress.health_check | Running post-update validation |
updates.progress.apply_done | Update completed successfully |
POST /system-updates/auto-update
Section titled “POST /system-updates/auto-update”Toggle automatic updates.
| Parameter | Type | Values |
|---|---|---|
auto_update | string | 1 to enable, 0 to disable |
Common flows
Section titled “Common flows”Starting a conversation and streaming the response
Section titled “Starting a conversation and streaming the response”POST /api/v1/conversations— create a conversation, note itsidGET /api/v1/conversations/{id}— fetch the snapshot, noteevent_cursorGET /api/v1/events?cursor={event_cursor}— open the SSE streamPOST /api/v1/conversations/{id}/messages— send a message with a freshclient_message_id- Listen on the SSE stream for
message.deltachunks,run.output_finished, andrun.completed
Handling a tool approval
Section titled “Handling a tool approval”- You receive a
run.approval_requiredevent on the SSE stream - Show the user the approval card with the tool name and arguments
POST /api/v1/approvals/{approval_id}/decisionwith{ "approved": true/false }- The run resumes and the SSE stream continues
Reconnecting after a disconnect
Section titled “Reconnecting after a disconnect”Keep track of the last event id you received from the SSE stream. On reconnect:
GET /api/v1/conversations/{id}to refresh the snapshot (catches up on any messages you missed)GET /api/v1/events?cursor={last_seen_id}to resume the stream from where you left off
Paginating the conversation list
Section titled “Paginating the conversation list”GET /api/v1/conversations— first page (most recent 50 conversations)- If the response includes
next_cursor, pass it as?before={next_cursor}to fetch the next page - Repeat until
next_cursoris absent
Agent update lifecycle
Section titled “Agent update lifecycle”POST /agents/update/start— returnsjob_idGET /agents/update/status/{job_id}— poll every 1–2 seconds untildone: true- Check
successand followredirect_toif present
Authentication
Section titled “Authentication”All endpoints require a valid session cookie obtained from signing in at /login.
Admin endpoints additionally require is_admin = true on the user account. Non-admin requests to admin endpoints return HTTP 403.