Skip to content

HTTP API

The Engine HTTP API powers the Web UI and can be used to build custom integrations. It’s a REST API with Server-Sent Events (SSE) for streaming responses.

http://localhost:3000/api

When running the Web UI image, the API is proxied through SvelteKit. When running --http-api only, it defaults to port 3100.

In single-user mode (default), no authentication is required. The API is intended for local use only.

GET /health
GET /api/health

Returns {"status":"ok"}. No auth required, useful for Docker health checks.

POST /api/sessions # Create a new session
DELETE /api/sessions/:id # Delete a session
POST /api/sessions/:id/run # Run a task (SSE streaming response)
GET /api/sessions/:id/pending-prompt # Check for a resumable prompt
POST /api/sessions/:id/reply # Reply to a pending prompt
POST /api/sessions/:id/abort # Abort a running task
POST /api/sessions/:id/compact # Compact context window
Terminal window
curl -N -X POST http://localhost:3000/api/sessions/{id}/run \
-H "Content-Type: application/json" \
-d '{"task": "What is the weather in Munich?"}'

The response is a Server-Sent Events stream with these event types:

Event Description
text Streamed response text chunk
thinking Extended thinking summary
tool_call Tool invocation (name, input)
tool_result Tool result (output, success)
prompt Agent requests user input (ask_user). Includes promptId for resumable prompts
secret_prompt Agent requests a secret (ask_secret). Includes promptId
turn_end Turn completed
changeset_ready File changes pending review (accept/rollback). Includes the runId the review answer must send back
done Run completed
error Error occurred
GET /api/threads # List threads
GET /api/threads/:id # Get thread details
PATCH /api/threads/:id # Update (rename, archive)
DELETE /api/threads/:id # Delete thread
GET /api/threads/:id/messages # Get messages (supports pagination)
GET /api/threads/:id/debug-export # Full debug bundle (thread + messages + per-run
# cost/tokens/cache/composition + compaction events;
# secrets scrubbed)
GET /api/memory/:ns # Read namespace (knowledge|methods|status|learnings)
PUT /api/memory/:ns # Replace namespace content
POST /api/memory/:ns/append # Append to namespace
PATCH /api/memory/:ns # Update (old/new text)
DELETE /api/memory/:ns # Delete entries (pattern query param)
GET /api/secrets # List secret names
GET /api/secrets/status # Secret status overview
PUT /api/secrets/:name # Store a secret
DELETE /api/secrets/:name # Delete a secret

Prompts (ask_user and ask_secret) are persisted in SQLite and survive SSE disconnects, page refreshes, and thread switches. The agent polls the database for answers instead of holding an in-memory callback.

  1. Agent calls ask_user or ask_secret → prompt written to SQLite with a promptId
  2. SSE event sent to client (best-effort — client may not be connected)
  3. Agent polls SQLite every 2s for an answer
  4. If client disconnects, the agent loop stays alive (polling is near-zero CPU)
  5. Client reconnects → GET /api/sessions/:id/pending-prompt → sees the prompt
  6. Client replies → POST /api/sessions/:id/reply with promptId → answer written to SQLite
  7. Agent picks up answer on next poll → resumes execution

Prompts expire after 24 hours. On engine restart, all pending prompts are expired.

Terminal window
GET /api/sessions/:id/pending-prompt

Returns {"pending": false} or the full prompt data:

{
"pending": true,
"promptId": "uuid",
"promptType": "ask_user",
"question": "Shall I create the task?",
"options": ["Yes", "No"],
"timeoutMs": 86400000,
"createdAt": "2026-04-03T23:30:00Z"
}

Include promptId for idempotent replies (prevents double-answer race conditions):

Terminal window
POST /api/sessions/:id/reply
Body: {"answer": "Yes", "promptId": "uuid"}

During a run, the agent may request a secret via the ask_secret tool. This triggers a secret_prompt SSE event:

event: secret_prompt
data: {"promptId":"uuid","name":"STRIPE_API_KEY","prompt":"Enter your Stripe API key","key_type":"stripe"}

The client stores the secret directly via PUT /api/secrets/:name (the value never enters the SSE stream), then confirms:

POST /api/sessions/:id/secret-saved
Body: {"saved": true, "promptId": "uuid"}
GET /api/config # Get config (secrets redacted)
PUT /api/config # Update config
GET /api/history/runs # List runs (filterable)
GET /api/history/runs/:id # Run details
GET /api/history/runs/:id/tool-calls # Tool calls for a run
GET /api/history/stats # Aggregated statistics
GET /api/history/cost/daily # Daily cost breakdown
GET /api/kg/stats # Graph statistics
GET /api/kg/entities # List/search entities
GET /api/kg/entities/:id # Entity details + relations
GET /api/tasks # List tasks
POST /api/tasks # Create task
PATCH /api/tasks/:id # Update task
DELETE /api/tasks/:id # Delete task
POST /api/tasks/:id/complete # Mark complete
POST /api/tasks/:id/stop # Stop the run in flight (202 = requested, not completed)
GET /api/triggers # List schedules, watches and reminders

GET /api/triggers marks each row with running (a run of it is in flight) and stoppable (a stop can end that run; false for backups and reminders). Someone you let in sees both only for runs they started themselves.

POST /api/tasks/:id/stop answers 202 when the stop was requested; the run can still finish on its own. A 409 carries a code: not_running (nothing to stop), unstoppable (the run is in a phase a stop cannot reach) or ambiguous_id (a short id that matches more than one task). A 403 means the caller neither owns the instance nor started the run.

GET /api/artifacts # List artifacts
POST /api/artifacts # Save artifact
GET /api/artifacts/:id # Get artifact
DELETE /api/artifacts/:id # Delete artifact
GET /api/merges # Merges that can be taken back, newest first
POST /api/merges/:id/rollback # Take one merge back

A merge of two contacts or organisations writes a reversal record (~/.lynox/sweeps/merge-*.json). It holds both entries’ detail rows (email and phone for a person, domain and VAT number for an organisation), because taking the merge back has to restore them. These routes never return those rows.

A merge is listed with:

  • the two names. That includes the merged-away entry’s name, which the merge removed from the contact views; a merge cannot be named for taking back without it.
  • its date;
  • how many data rows and conversation links it moved;
  • three flags. applied is false for a merge that did not complete. inEffect says the merge still holds and this record can take it back. superseded says the same two entries were merged again later, and only the newest record can take a merge back.

inEffect is read from the contact graph, with the same check the rollback makes before it acts, so a merge already taken back says so. A rollback that would move data rows or conversation links is refused while those stores are not available, rather than reversing only the contact graph.

Records older than 90 days are removed at the next merge or engine start, the newest one excepted. A merge whose record is gone is not listed and can no longer be taken back.

An instance has one contact graph, so every signed-in user sees the same merges. A refusal is one of fixed reasons, never the store’s own message.

GET /api/crm/contacts # List contacts
GET /api/crm/contacts/:name/interactions # Contact history
GET /api/crm/contacts/:name/deals # Contact deals
GET /api/crm/deals # List deals
GET /api/crm/stats # CRM statistics
GET /api/google/status # Google auth status
POST /api/google/auth # Start device flow
POST /api/google/revoke # Revoke auth; {"delete_drive_backups": true} deletes lynox's Drive backups first
POST /api/google/reload # Reload Google integration
GET /api/google/oauth-url # Get OAuth start URL (managed hosting)
POST /api/google/claim-managed # Claim tokens from control plane (managed hosting)
POST /api/searxng/check # Validate SearXNG URL
GET /api/backups # List backups
POST /api/backups # Create backup
POST /api/backups/:id/restore # Restore backup
GET /api/files # List directory
GET /api/files/download # Download file
GET /api/files/read # Read file preview (max 1 MB)
DELETE /api/files # Delete file
GET /api/workflows # List workflow runs
GET /api/workflows/:id # Workflow details
GET /api/workflows/:id/steps # Workflow step results
GET /api/workflows/stats/steps # Step statistics
GET /api/workflows/stats/cost # Workflow cost stats
GET /api/datastore/collections # DataStore collections
GET /api/datastore/:collection # Collection records
GET /api/vault/key # Retrieve vault key
POST /api/vault/rotate # Rotate vault key
GET /api/auth/token # Generate/retrieve auth token
GET /api/export # Data export (Art. 15 + Art. 20)
DELETE /api/data # Data deletion (Art. 17)

Zero-knowledge self-hosted→managed transfer via X25519 ECDH + AES-256-GCM.

GET /api/migration/preview # Preview available databases and sizes
POST /api/migration/export # Start migration export (SSE streaming)
GET /api/migration/handshake # Initiate ECDH key exchange (import side)
POST /api/migration/handshake # Complete ECDH key exchange (import side)
POST /api/migration/manifest # Receive migration manifest
POST /api/migration/chunk # Receive encrypted data chunk
POST /api/migration/restore # Restore imported data
GET /api/sessions/:id/changeset # Pending file changes and the runId they belong to
POST /api/sessions/:id/changeset/review # Accept/rollback file changes

The review body is { "action": "accept" | "rollback" | "partial", "runId": "<from the GET>", "rolledBackFiles": ["<path>"] } (rolledBackFiles for partial only). The answer is refused with 409 when runId is missing or names an earlier run than the one holding the changes, and while a run is in progress.

POST /api/transcribe # Transcribe audio → text (STT, base64 in)
POST /api/speak # Synthesize text → speech (TTS, SSE audio stream; needs MISTRAL_API_KEY)
GET /api/thread-insights # Thread analytics
GET /api/patterns # Detected patterns
GET /api/metrics # Metrics data
GET /api/api-profiles # API Store profiles
GET /api/api-profiles/:id # Individual API profile