API Reference

Endpoint map and auth behavior.

Email reply suggestions

POST /api/emails/:id/reply-suggestion?scope=personal|agent&mailbox=ADDRESS&folder=inbox&ui_language=de

Authenticated (modules:api:write for module tokens). JSON body: { "existing_reply": "", "reply_language": "en" }. The current editor text is optional, limited to 20,000 characters, and guides the reply. The source email is fetched with existing provider authorization; a requested mailbox must match the loaded source. Returns { "body": "...", "source_language": "en", "reply_language": "en" }. Errors: 400 invalid input, 401 unauthenticated, source-loading errors (including 404 mailbox mismatch), 502 generation failure with localized message. No draft is saved or sent, and no workflow decision changes. Composer saves preserve AI attribution with auto_generated: true on POST /api/drafts or PATCH /api/drafts/:id; patching never clears existing AI attribution. Email and attachment content are reference data, not instructions.

GPT-Live voice transport

The Codex OAuth selection uses gpt-live-1-codex. POST /api/chat/live/webrtc accepts the same { sdp, voice } input and returns only { session: { id }, transport: { type: "webrtc", sdp } }. Model and credentials are server-owned. DELETE /api/chat/live/webrtc/[sessionId] closes an owner-scoped subscription call; unknown or other users' IDs have no effect. Subscription relay responses keep the existing PCM24k / clapilot_delegate contract. Sessions are capped at 30 minutes.

  • POST /api/chat/live/session: when the selected model is gpt-live-1 or gpt-live-1-codex, returns realtime_api: "live", transport: "openai_webrtc", and a same-origin webrtc_url; no ephemeral or project secret is returned.
  • POST /api/chat/live/webrtc: authenticated JSON { "sdp": "v=0...", "voice": "marin" } (voice is optional), at most 65,536 SDP characters. The server resolves the configured OpenAI model and creates the Live session. Returns { "session": { "id": "..." }, "transport": { "type": "webrtc", "sdp": "..." } }. Model, tools and credentials cannot be overridden by the request. Unauthorized requests return 401, invalid configuration/SDP 400, and upstream failures 502.
  • POST /api/chat/live/relay/session: native clients receive realtime_api: "live", transport: "clapilot_relay", an owner-scoped relay ID and PCM 24 kHz audio metadata. Existing relay input/events/delete routes apply. The server translates audio/control events to Live and delegates through the existing application tool contract.
  • GET /api/chat/live/relay/ws?session=<relay_session_id>&ticket=<ws_ticket>: WebSocket transport for the same relay session, used by voice devices. The relay session response carries ws_path and a one-time ws_ticket; the upgrade is answered by scripts/standalone-ws-server.mjs (the container entry that wraps the Next standalone server, because route handlers cannot upgrade), which compares the ticket in constant time, replays the session backlog, forwards client frames to the provider socket and relay events back. Binary client frames are treated as raw PCM16 and wrapped into input_audio_buffer.append with base64 server-side; text frames are forwarded verbatim. Unknown session or wrong ticket returns 401 on the upgrade. The POST input route and the SSE events route stay available and behave identically.
  • GPT-Live relay SSE adds clapilot.live.transcript.updated with { id, role, text, pending: true } for cumulative utterance text. clapilot.live.transcript.segment finalizes the same ID with pending: false. IDs are connection-local; clients scope them to the live connection. Finalization uses punctuation/quiet-period heuristics and does not indicate audio playback completion. Older clients can continue consuming final segments.
  • /api/clapilotaicore/live-voice-settings and /api/call-agent/config include gpt-live-1 in their OpenAI voice model options. gpt-live-transcribe remains a separate transcription model.

Data portability export

GET /api/data-export?scope=personal|workspace and POST /api/data-export create the same non-mutating ZIP snapshot; POST accepts { "scope": "personal" } or { "scope": "workspace" }. Only admins may request the workspace scope. GET is intended for browser-native streamed downloads that do not buffer the archive in JavaScript. The archive contains complete JSON and per-entity CSV (including scheduled automation definitions, personal/group chat messages, agent runs, and installed data-bearing bundled-module datasets), ICS, VCF, document and referenced media binaries, a manifest, and a README. Personal scope includes automations created by the requester; workspace scope includes all definitions. Prompts, triggers, schedules, workflow graphs, and delivery targets remain portable while webhook, Bearer, and other structured access tokens are redacted. manifest.module_export lists included installed modules and datasets. Complete JSON and CSV artifacts are serialized row by row into temporary files before Archiver reads them, avoiding a second whole-export string in the web-process heap. Personal exports include Social Media library files referenced by the requester's posts; workspace exports include the complete Social Media media library, including unused uploads. Documents referenced by exported Tax Manager receipts, generations, and export jobs are included only when the requester retains normal shared/owner visibility. Requester-owned generated-image links in personal chat messages and module records are resolved into portable files while prompt, provider, model, storage-path, and image metadata remain redacted from the exported asset rows. Social-media session data and OAuth states are excluded with all other authentication secrets. A convenience XLSX is included only while its bounded row/cell/input-size plan is safe to materialize; manifest.tabular_formats.xlsx records inclusion or the limit that caused omission. Personal exports redact private Team Chat run recovery/client context and tool results belonging to other participants. Legacy open Team Chat membership rows retain joined_at; boundary_ambiguous=true identifies rows whose pre-history rejoin gaps cannot be reconstructed, instead of treating read-receipt updated_at activity as a join boundary. Authentication secrets and private agent memory are excluded. The response is application/zip with Cache-Control: no-store. See Daten-Portabilität und Exit-Plan for archive semantics and the master/sync distinction.

This page inventories the HTTP surface of Clapilot: first-party app APIs, bundled-module APIs, hub/distribution APIs, and the internal native-runtime endpoints the app talks to.

GET /api/hub/fleet/instances and GET /api/hub/fleet/instances/:id include health per instance while preserving raw connector status. Health contains unified status, deciding source, ISO last_checked_at (newest process report or available check), diverged, machine-readable reasons, and process, external_check, and telemetry breakdowns. These expose process report and machine heartbeat times, HTTP result/latency/check time, and telemetry state/time/age. Fresh reachable checks prevent connector errors alone from reporting the instance as down; external checks older than 15 minutes fall back to process status.

POST /api/hub/fleet/instances/:id/actions accepts the existing stop, start, and redeploy actions. The admin-only set-shell-tools action additionally requires boolean shellToolsEnabled; it persists the capability policy for that one Fleet instance and redeploys it. If the instance is stopped, connector 1.1.0 or newer recreates the deployment with --no-start so the policy change does not start the tenant. Older or unreported connector versions are rejected with 409 fleet_connector_update_required before a job is queued; re-running the served Fleet connector installer downloads the current connector and safely restarts its user service. Omitting the boolean is rejected with 400.

POST /api/hub/fleet/agents/registry-credentials uses the same per-machine HMAC contract and returns { ok, ghcr: { username, token } | null, watchtowerConfigJson: string | null } — the same GHCR pull credentials served at enrollment. Connector 1.4.0+ calls it at startup when the machine's Docker config lacks a ghcr.io login or the Watchtower registry config file is missing, so an enroll-time docker login failure self-heals.

POST /api/hub/fleet/agents/heartbeat uses the per-machine HMAC contract and returns { ok, connector: { latestVersion }, remoteAgent: { enabled, label } }. latestVersion is the bundled connector's x.y.z version or null when the Hub cannot read it. remoteAgent reflects the machine's persisted opt-in and current name. Probe payloads may include codexVersion; an explicit string persists it, while explicit null or an empty string clears it. Older connectors that omit the key leave the stored value unchanged.

PATCH /api/hub/fleet/machines/:id accepts remoteAgentEnabled (or remote_agent_enabled) as a boolean in addition to the existing machine fields. GET /api/hub/fleet/connector/files/:name serves only clapilot-fleet-connector.mjs, clapilot-fleet-event-outbox.mjs, and agent-orchestrator-codex-remote-runner.mjs; all other names return 404. The legacy single-file GET /api/hub/fleet/connector endpoint remains available for old installers.

GET /api/clapilot-code/install serves the dependency-free clapilot-code installer with the request-facing instance origin substituted into its login instruction. It requires no interactive session or API key, but returns 404 unless app_settings.developer_mode_enabled is true.

GET /api/clapilot-code/files/:name serves exactly clapilot-code.mjs from the image's scripts/ directory. The literal allowlist rejects every other filename with 404. The route requires no interactive session or API key and shares the install route's Developer-mode 404 gate. The downloaded CLI uses a separately created inference:execute instance API key for model requests; the code-download route never accepts or exposes that key.

The /api/modules/agent-orchestrator/api/remote-runners/* family accepts either the existing Hub shared-secret HMAC or a Fleet machine HMAC. For Fleet authentication, x-clapilot-instance-id must be that machine's UUID and the signature uses its per-machine secret. Fleet credentials are rejected for every other module API path.

  • Source of truth: route handlers under src/app/api/**/route.ts (bundled-module endpoints are served through src/app/api/modules/[slug]/api/[...endpointPath]/route.ts from bundled-modules/<slug>/), plus the native clapilot-agent service for /internal/*.

  • /api/modules/agent-orchestrator/api/sessions and /api/modules/agent-orchestrator/api/sessions/:id

    • Session payloads include usageJson and runDiagnostics from the latest associated agent_runs row. Failed-run diagnostics are therefore available consistently to web, iOS, and macOS detail views. Runtime-authored runDiagnostics carry phase (provider | tool | runtime), errorSource (user | runtime | memory | tool | provider), and error.exceptionName / error.exceptionCode / error.toolName / error.toolCallId next to the persisted error_code / error_message pair. runDiagnostics.provider additionally records the provider fallback chain (requestedProvider, finalProvider, fallbackProvider, fallbackUsed, fallbackChain, rootCause, finalFailure), and a failed run's effectiveModel names the provider the run actually ended on; see Agent Tool Contracts → Failed run diagnostics.
  • Organization: endpoints are grouped by feature area (Auth, Documents, Tasks, E-Mail, Chat, integrations, Hub, Admin, module platform), followed by the internal native-runtime section.

  • Not every endpoint carries a full request/response spec here; entries prioritize auth level, methods, and behavioral contracts that agents and clients rely on. Agent-facing tool contracts live in Agent Tool Contracts.

Product telemetry

POST /api/telemetry/events stores a validated product event for the authenticated user. The endpoint accepts metadata only:

{
  "event_name": "module_opened",
  "session_id": "browser-session-id",
  "module": "athlete-brand-matching",
  "route": "/modules/athlete-brand-matching",
  "success": true,
  "duration_ms": 120,
  "error_code": null,
  "properties": {
    "flow": "athlete-brand-matching",
    "step": "opened",
    "module_slug": "athlete-brand-matching"
  }
}

Unknown event names are rejected. Content-like or secret-like properties are dropped before storage.

GET /api/telemetry/report?from=<iso>&to=<iso> returns aggregate usage and friction metrics for the authenticated user. Admins may pass user=<uuid|email|name> (legacy user_id=<uuid> is still accepted) to scope the report to a specific user.

GET /api/telemetry/product-health?from=<iso>&to=<iso>&user=<uuid|email|name> returns the internal Product Health report for admins only. The response reuses product_events aggregates and adds last activity, active user/session lists, latest event metadata, and a timeseries array (bucketInterval of hour or day) for the tracking dashboards under /admin/hub. The user filter accepts a UUID for an exact match or a free-text fragment matched against email and display name; legacy user_id=<uuid> is still accepted. Non-admin users receive 401 or 403 from the server-side role check.

POST /api/telemetry/hub-sync (admin only) triggers an immediate push of pending local product events, model-usage logs, chat transcripts, and the due cached Subscription Usage snapshot to the configured remote hub. The response preserves the combined { ok, skipped, synced, error } fields and adds the individual results as { productTelemetry, modelUsage, chatTranscripts, subscriptionUsage }, each with { ok, skipped, synced, error }. Subscription Usage keeps its own persisted 15-minute report lease, so this action does not bypass its provider-refresh limit. The same combined sync runs automatically in the background after events are recorded when hub_mode is remote.

POST /api/hub/telemetry/ingest (hub instances only, HMAC-signed) accepts telemetry batches from connected instances: { instanceUrl, events: [{ id, createdAt, userId, userEmail, userName, sessionId, eventName, module, route, properties, durationMs, success, errorCode, platform }] }. Events are stored idempotently in hub_instance_product_events keyed by (instance_host, source_event_id), and the sender is auto-registered as a monitored instance with discovery source telemetry.

PATCH /api/hub/health/instances/[id] with issueTelemetryCredential: true lets a hub admin issue or reset a random per-instance telemetry credential. The plaintext value is returned only in that admin response, while the hub stores only its hash, binds it to the visible sender identity, and disables monitoring for explicit re-approval. The credential is copied to the customer instance through the remote-hub settings and stored there encrypted via POST /api/telemetry/hub-credential; GET returns only whether it is configured. Reissuing provides the authenticated recovery path after token loss.

POST /api/hub/telemetry/enroll validates an already admin-issued credential against the stable x-clapilot-instance-id; fleet-wide HMAC authentication alone cannot create or replace the binding.

POST /api/hub/telemetry/ingest and POST /api/hub/telemetry/model-usage/ingest additionally require x-clapilot-instance-credential. The credential must match both the hub-side hash and the immutable sender identity approved for that monitored instance, so one spoke cannot submit monitoring telemetry for another host.

POST /api/hub/telemetry/model-usage/ingest (hub instances only, HMAC-signed and per-instance authenticated) accepts { instanceUrl, logs: [...] }, with each log limited to { id, createdAt, userId, userEmail, userName, providerType, providerSlug, providerLabel, requestedModel, effectiveModel, requestKind, transport, status, inputTokens, outputTokens, totalTokens, cacheCreationInputTokens, cacheReadInputTokens, durationMs }. Prompt/session data, endpoints, errors, raw usage payloads, prompt-layer details, and metadata are not accepted by this contract. The hub validates UUIDs/timestamps, clamps non-negative integer counters, truncates text fields, stores rows idempotently in hub_instance_model_usage_logs by (instance_host, source_log_id), and returns { ok, received, accepted, inserted }.

POST /api/hub/subscription-usage/ingest (hub instances only, HMAC-signed and per-instance authenticated) accepts { instanceUrl, snapshot }. The sender normalizes the cached Codex, Claude Code, Grok, Ollama, and Cursor snapshot before transmission; credentials and arbitrary provider metadata are never accepted. The Hub validates and normalizes the snapshot again, then replaces the latest row for that instance in hub_instance_subscription_usage_snapshots. The endpoint returns { ok, accepted, checkedAt }.

GET /api/hub/telemetry/instances (admin only) lists the local instance plus, in local hub mode, all monitored instances with telemetry summaries (totalEvents, eventsLast7d, activeUsersLast7d, tokensLast7d, lastEventAt) and health/monitoring metadata. Monitoring metadata includes the persisted classification, separate periodic status/summary/check time, telemetry state (never, fresh, stale), latest hub receipt time, 24-hour error rate/operation count, and per-host thresholds. tokensLast7d is the sum of total_tokens reported during the previous seven days.

The persisted monitoring_payload (not the telemetry instances response) diagnoses public failures with failure_type values http_abort, tunnel_failure, or origin_failure. Fleet recovery requires three consecutive HTTP 200 responses after redirects (connector 1.4.7); its failed job result includes host, tunnel, origin, and per-container state; the latest failed restart diagnostics are included in incident escalation. result.recoveryDiagnostics.host adds hostname, project, connectorVersion, dockerReachable, composeFilePresent, and inspectError; reachable now reports connector execution independently of Docker access. Container inspection uses Docker project labels even when the Compose file is missing. external records a separate public probe when Compose fails before the HTTP gate. An open monitoring outage also needs three HTTP 200 observations before closing. The shared Fleet health resolver requires an actual successful HTTP status for a fresh external check. A missing or malformed HTTP status is down, including legacy payloads labelled healthy or degraded. This applies to Fleet API and fleet_list consumers; it does not itself close or reopen outage tasks. See Fleet recovery evidence for the recovery evidence required before closure.

GET/PUT /api/hub/monitoring/alert-room (admin only) reads and sets the Team Chat channel that receives hub customer monitoring alerts and instance storage alerts (app_settings.monitoring_alert_room_id). Only existing, non-archived public/private channels are accepted; at delivery time an inactive room falls back to clapilot-members with a logged monitoring_alert_room_fallback event so alerts are never lost.

PATCH /api/hub/health/instances/[id] (admin only) updates only the supplied instance credentials and monitoring fields, so concurrent edits do not restore stale values. Monitoring fields are classification (kunde|eigen|dev|intern), monitoringEnabled, telemetryStaleAfterHours (default 26), errorRateThreshold as a fraction (default 0.15), errorRateMinOperations (default 20), and the one-shot issueTelemetryCredential operation described above. Enabling monitoring explicitly approves the current target for scheduled outbound probes; auto-discovered targets start disabled. Explicit monitoringEnabled: false records monitoring_deactivated_at, allowing linked outage tasks to close for a confirmed retired host. Re-enabling clears that marker; automatic credential-reset suspension does not set it. Invalid classification, threshold, identity, and credential responses are localized from the request UI language. This admin contract is intentionally not exposed as an agent tool.

Outage task writes (POST /api/aufgaben, PATCH /api/aufgaben/[id], and status-category changes/reassignments under /api/aufgaben/statuses/[key]) return HTTP 409 with { error: <localized message>, code: "HUB_OUTAGE_RECOVERY_REQUIRED" } when completion lacks recovery evidence. Register affected hosts with task tags monitoring-outage:<hostname>; legacy outage titles naming a registered host are linked automatically. Associations persist after tags/titles change. Completion requires three consecutive healthy external HTTPS 2xx checks after task creation, spaced 4–10 minutes apart, latest at most ten minutes old, or explicit administrative monitoring deactivation. The same database gate applies to chat/live tools and custom done status categories. Missing monitoring records and credential-reset suspension fail closed. See Hub outage completion.

POST /api/hub/chat-transcripts/ingest (hub instances only, HMAC-signed) accepts chat transcript batches from connected instances: { instanceUrl, records: [{ id, source, createdAt, sessionKey, sessionTitle, channelType, model, userId, userEmail, userName, messages, metadata: { toolTrace, ... } }] } with source in personal_chat|group_chat|agent_run. messages accepts user, assistant, and tool roles. Tool-calling assistants use { role: "assistant", content: "", tool_calls: [{ id, type: "function", function: { name, arguments: {} } }] }; tool results use { role: "tool", tool_call_id, name, content }. Camel-case toolCalls/toolCallId is also accepted, but function arguments must be JSON objects. Max 200 records per request and 200 messages per record; inline data: base64 URIs are stripped server-side and message content is capped. Invalid tool-call messages are discarded. Records are stored idempotently in hub_instance_chat_transcripts keyed by (instance_host, source, source_record_id); the response is { ok, received, accepted, inserted }.

GET /api/hub/chat-transcripts/stats (admin only) returns { hub_mode, chat_transcript_sync_enabled, instances: [{ instanceHost, isLocal, records, sessions, lastReceivedAt, sources: [{ source, records, sessions, lastReceivedAt }] }], totals }, always including the local instance and, in local hub mode, all instances that have synced transcripts.

GET /api/hub/chat-transcripts/export?instances=<csv>&sources=<csv>&from=<iso>&to=<iso>&format=native|messages (admin only) streams an application/x-ndjson download combining hub-synced spoke transcripts and the hub's own local chats (instances=local restricts to local data). format=native remains the full-fidelity session archive, now including tool messages and a session-level toolTrace: "complete"|"unavailable"; it never drops records. format=messages emits training samples with a leading current-persona system message, complete user/tool/assistant turns, and a top-level tools schema list whenever calls are present. Samples are windowed at complete-turn boundaries with a target maximum of 32 messages; a single larger turn remains intact. Records whose tool trace cannot be linked or is incomplete are dropped, and traceable segments on either side become separate samples. Rows synced before metadata.toolTrace existed are treated as unavailable.

A tool-bearing format=messages line has this shape:

{"tools":[{"type":"function","function":{"name":"calendar_list_events","description":"…","parameters":{"type":"object","properties":{"start":{"type":"string"}},"required":["start"]}}}],"messages":[{"role":"system","content":"Du bist Angela, der Clapilot-Copilot. …"},{"role":"user","content":"Prüfe meinen Kalender"},{"role":"assistant","content":"","tool_calls":[{"id":"call_1","type":"function","function":{"name":"calendar_list_events","arguments":{"start":"2026-07-24"}}}]},{"role":"tool","tool_call_id":"call_1","name":"calendar_list_events","content":"[]"},{"role":"assistant","content":"Es gibt keine Termine."}]}

The training contract has three strict rules: function.arguments is a JSON object, never a JSON-encoded string; every role:"tool" message has string content (structured source values are serialized); and tools is a top-level sibling of messages. Call IDs are renumbered per row and every row ends with a non-empty assistant response. Rows without calls omit tools.

Adding dryRun=1 runs the same selected messages pipeline without streaming a file and returns JSON: { ok, format, conversations, messages, toolCalls, toolMessages, conversationsWithTools, droppedTurnsUntraceable, sessionsWithDroppedTurns, runsWithIncompleteTrace, toolSchemasFromRegistry, toolSchemasDerived, derivedToolNames, suspiciousWithoutToolCall }. derivedToolNames is capped at 100 names; suspiciousWithoutToolCall counts emitted rows whose assistant text refers to likely calendar, mail, task, document, or participant data but has no tool result message.

GET /api/hub/telemetry/instances/[id]/report?from=<iso>&to=<iso>&user=<uuid|email|name> (admin only) returns the per-instance tracking report. id=local reads local product_events and agent_model_request_logs; a monitored instance UUID reads the hub-synced product and model-usage tables (requires local hub mode). The response shape matches the Product Health report plus an instance descriptor and a tokenUsage section:

{
  "tokenUsage": {
    "totals": {
      "requests": 42,
      "inputTokens": 120000,
      "outputTokens": 18000,
      "totalTokens": 138000,
      "cacheReadTokens": 24000,
      "cacheWriteTokens": 5000
    },
    "byModel": [
      {
        "providerType": "openai",
        "providerSlug": "openai-main",
        "providerLabel": "OpenAI",
        "model": "gpt-5.1",
        "requests": 42,
        "inputTokens": 120000,
        "outputTokens": 18000,
        "totalTokens": 138000
      }
    ],
    "byProvider": [],
    "timeseries": [
      { "bucket": "2026-07-17T10:00:00.000Z", "inputTokens": 120000, "outputTokens": 18000, "totalTokens": 138000 }
    ]
  }
}

The model and provider arrays contain at most 15 entries ordered by total tokens. Token aggregation includes all request statuses and uses the same hour/day bucket interval as product-event activity.

Auth behavior

  • protected APIs return 401 when unauthenticated
  • admin routes enforce requireAdminRole() checks
  • session context resolves from clapilot_session cookie
  • routine bootstrap admin initialization retains the existing password hash and session version when the configured password is unchanged. A genuinely changed configured password still updates the hash and revokes old sessions
  • if session verification cannot reach the database, protected requests return 503 with Retry-After: 5 and Cache-Control: no-store, without deleting the cookie or redirecting to login. Protected API handlers also translate a failed user/profile lookup after middleware verification into this response. Web clients treat these failures as errors rather than a signed-out user; native iOS/macOS clients retain their cookie on non-401 responses. Requests remain denied until verification succeeds; confirmed expiry, invalid signatures, deleted users and password-change revocation still require login
  • web chat, floating chat and profile/settings initialization display a localized connection message and retry transient transport failures and HTTP 429/5xx session reads every five seconds until they succeed or the screen unmounts. Permanent HTTP failures such as 403/404 stop retrying and show the error. Native iOS/macOS session restoration also retries temporary /api/auth/me and network failures, respecting a bounded Retry-After; changing instances or signing in manually cancels the previous restoration. A localized unavailable state offers Change instance on iOS/macOS, including the macOS menu bar, so an unreachable saved server cannot trap startup. Other native session-read callers keep single-request error handling. Retries do not submit credentials or repeat mutations
  • native runtime service auth uses short-lived Bearer tokens for module runtime APIs; remote slave/client module calls may also use Hub HMAC headers derived from CLAPILOT_HUB_SHARED_SECRET
  • paired voice devices authenticate with Authorization: Bearer clpd_... device tokens (SHA-256 hashed at rest, shown once at pairing). A device token resolves to its owning user but is accepted only on a path allowlist: /api/voice-devices/me, /api/voice-devices/heartbeat, GET /api/voice-devices/reply-audio, POST /api/chat, /api/chat/stop, /api/chat/audio/*, /api/chat/sessions/*/messages, /api/chat/live/relay/**, and /api/chat/live/tools; every other route returns 401 for a device token. See Voice devices and the Voice Devices feature page

Database TLS verification

TLS database connections validate certificates and hostnames in web, agent, streamer and Google sync pools. DATABASE_SSL=true enforces TLS even when the URL requests disabling it. URL TLS settings cannot disable verification. Private CAs use DATABASE_SSL_CA (PEM) or DATABASE_SSL_CA_FILE; URL client certificate/key files remain supported. Install the correct CA before upgrading remote TLS deployments.

Desktop updates

The Sparkle update endpoints are public (auth: none). They expose signed release metadata and archives while keeping the private-repository GitHub token on the server.

  • GET /api/desktop-updates/{app}/appcast.xml
    • app must be clapilot or remote-runner; other values return 404
    • returns the newest matching appcast-clapilot.xml or appcast-remote-runner.xml from up to ten recent GitHub releases as application/xml; charset=utf-8
    • returns 404 when no recent release contains that appcast, 503 when GITHUB_RELEASES_TOKEN is not configured, and 502 when GitHub cannot be reached successfully
  • GET /api/desktop-updates/{app}/download/{filename}
    • filename must be one traversal-free path segment ending in .zip or .delta
    • clapilot accepts Clapilot-* assets but excludes Clapilot-Remote-Runner-*; remote-runner accepts only Clapilot-Remote-Runner-*
    • streams the exact matching asset from up to ten recent releases as application/octet-stream, with Content-Disposition and Content-Length when known; the server does not buffer the full archive
    • returns 404 for an invalid/unknown asset, 503 when the server token is missing, and 502 for an upstream GitHub failure

Response language

Routes that return localized text (errors, labels, generated preview lines) resolve the language in this order: ui_language in the JSON body, ui_language in the query string, then the clapilot_ui_language cookie the web app sets from the profile language. Without any of these they answer in German. The Apple clients append ui_language to every request.

Endpoint groups

Auth

Password change session revocation

Migration 349_user_session_revocation.sql adds a per-user session version and a database trigger that advances it for every password hash change, including admin, recovery and local Supabase paths. Cookie authentication checks this version against the database in middleware and server helpers; issuance cannot upgrade an old authentication version. Deploy the migration before the application. Existing cookies without a version are invalidated; web and Apple clients must sign in again. Successful login, reset and authenticated password changes issue a current-version cookie.

  • /api/auth/login
    • POST: rate-limited per client IP and per email with exponential backoff (in-memory per app instance). Once the free failed-attempt budget is exhausted, further attempts return 429 with a localized error and a Retry-After header (seconds); the lockout doubles per additional failure up to 15 minutes. A successful login clears the email counter. X-Forwarded-For/X-Real-IP are only honored when CLAPILOT_TRUST_PROXY_HEADERS=true (set this only when the app is reachable exclusively through a proxy that overwrites those headers); otherwise the per-IP dimension is disabled entirely — spoofable headers cannot bypass the limiter, and no shared bucket exists that an attacker could lock to deny login to everyone. Without trusted headers, protection is per-email only. Known tradeoff: anyone who knows an email address can force that account into repeated lockouts (bounded at 15 minutes per episode); this is the standard cost of per-account throttling. The login form surfaces the localized 429 message including the retry window
  • /api/auth/password-setup/inspect (public)
    • POST { token }: checks a one-time invitation/reset link for the /set-password page. Returns { status: "valid", purpose: "invite" | "reset", email, display_name, language, expires_at } or { status: "invalid" | "expired" | "used" }. The token travels in the body, never in the API query string
  • /api/auth/password-setup (public)
    • POST { token, password, ui_language? }: redeems the link, stores the new password (minimum 6 characters), clears users.invitation_pending, marks the link used, revokes the user's other outstanding links, seeds first-login onboarding when enabled, and sets the session cookie like /api/auth/login. Responds with { purpose, user }; unusable links return 410 with code invalidToken / expiredToken / usedToken, short passwords 400 with passwordTooShort
  • /api/auth/logout
  • /api/auth/me
  • /api/auth/update
  • /api/profile
    • GET: return the signed-in user profile metadata used by /profil, including display_name, email, hosted_workspace (name, email and password then belong to the Clapilot account), avatar_url, per-user chat_preferences, normalized memoryPreferences.share_with_team (default true; sharing is opt-out), and agent_pet_key (bubbles by default). chat_preferences.speech_to_text_provider is device or openai_realtime and defaults to device
    • POST: update the signed-in user profile picture with { avatar_url }; accepts null to remove the stored picture and currently expects a PNG/JPG/GIF/WebP data URL payload. The same route also accepts { chat_preferences } for per-user chat UI preferences such as show_tool_calls, ui_language, and speech_to_text_provider, { memoryPreferences: { share_with_team } } for the new-memory team-sharing preference (enabled by default; explicit false opts out), and { agent_pet_key } for the chat activity Pet selection
  • /api/profile/pets
    • GET: return built-in and signed-in-user custom Profile Pets used by Settings -> Profile -> Pet; custom pets point at authenticated generated-image URLs
    • POST: register a generated image as a custom Profile Pet with { key|agent_pet_key, label|name, preview_image_id|image_id, activity_image_id?, description?, still_prompt?, animation_prompt?, select_for_profile? }. If activity_image_id is omitted or points to a static image, Clapilot derives a transparent looping laptop-working GIF from preview_image_id for the pet activity state.
  • /api/automation-result-targets
    • GET: list selectable result-delivery targets for automations. Returns Haupt-Chat, Teamchat #general, the signed-in user's accessible Teamchat channels/groups, approved Telegram/WhatsApp/Signal/iMessage DMs linked to the signed-in user, and approved Telegram/Slack/WhatsApp/Signal/iMessage/instance-bridge groups
  • /api/auth/agent/system-token (machine-token mint endpoint)
    • POST: mint a short-lived Bearer token for system/skill callers authenticated via x-clapilot-agent-system-secret; accepts scopes[], optional ttlSeconds, and optional subject to issue a user-scoped token for user-owned APIs such as app integrations
  • /api/push/devices
    • POST: register or refresh one signed-in Apple account/instance subscription with { installationId, instanceId, token, platform, bundleId, environment, deviceName?, appVersion? }. installationId is stable for the app installation; token rotation updates the shared installation record without replacing its other subscriptions
    • DELETE: unregister only the signed-in account/instance subscription with { installationId, instanceId, platform, bundleId }; other accounts on the installation remain subscribed
    • APNs payloads include the subscription's instance_id and an allowlisted target_route. Delivery selects only subscriptions whose user still exists in the current instance, and invalid APNs tokens deactivate the installation
  • /api/user/menu-preferences
    • GET: return the signed-in user's persisted sidebar/menu layout preferences (version: 1, groups[] with key, itemOrder, hiddenItems)
    • POST: save { preferences } with the same schema; invalid shapes return 400

Hosted portal

Only served when CLAPILOT_DEPLOYMENT_ROLE=portal; every route answers 404 on instance deployments. Errors return { code, error } with a stable code and a message localized from ui_language (body or query) or the UI language cookie. Portal sessions use the clapilot_account cookie; sign-in also sets the same portal token as clapilot_session for native apps. Instance session verification rejects it (it carries an audience), and the router strips both cookies before requests reach a cell. See Hosted portal for the product rules.

  • Public
    • GET /api/portal/registration: { registrationMode: "invite_code" | "open" | "closed" }
    • POST /api/portal/register { displayName, email, password, accessCode? }: always { status: "verification_sent" } on success, including for already registered emails; invalid_access_code, registration_closed (403), weak_password, invalid_email, rate_limited (429)
    • POST /api/portal/verify-email { token }: { status: "signed_in" } and the session cookie; invalid_token (404), expired_token (410)
    • POST /api/portal/verify-email/resend { email }: always { status: "sent_if_pending" }
    • POST /api/portal/login { email, password }: { status: "signed_in" }; invalid_credentials (401), email_not_verified (403), account_disabled (403), rate_limited (429)
    • POST /api/portal/logout
    • POST /api/portal/password/forgot { email }: always { status: "sent_if_exists" }
    • POST /api/portal/password/reset { token, password }: sets the password, marks the email verified, revokes other reset links, signs in
    • GET /api/portal/invites/{token}: { state: "invalid" } or { state: "valid" | "expired" | "used", teamName, inviterName, email, role, accountExists }
  • Signed-in account
    • POST /api/portal/invites/accept { token, displayName?, password? }: joins with the current session, or registers a verified account for the invited email when displayName/password are sent without a session; login_required (401), email_mismatch (403), account_in_other_team / already_member / team_full (409)
    • PATCH /api/portal/me { displayName?, language? }: own profile; returns { account }. The email cannot be changed here
    • POST /api/portal/password/change { currentPassword, newPassword }: wrong_password (400), weak_password (400), rate_limited (429) after repeated wrong current passwords; on success every other session of the account is signed out and the response sets a fresh cookie for this one
    • GET /api/portal/me: { account: { id, email, displayName, language, platformRole: "user" | "admin", isAdmin, team: { id, name, role, status, cellStatus, maxMembers } | null } }
    • GET /api/portal/team: { team: { …, memberLimit, canManage } | null, members[], invites[] } (invites only for owner/admin)
    • PATCH /api/portal/team { name }: owner only
    • POST /api/portal/team { accessCode? }: create a workspace for an account without a team (same registration-mode rules)
    • POST /api/portal/team/invites { email, role? }: owner (admin/member) or admin (member only); returns { invite: { id, email, role, emailStatus, fallbackLink } }, where fallbackLink is set when no email went out
    • POST /api/portal/team/invites/{id}/resend: rotates the token and restarts the 7-day validity
    • DELETE /api/portal/team/invites/{id}
    • PATCH /api/portal/team/members/{accountId} { role: "admin" | "member" } (owner), DELETE /api/portal/team/members/{accountId} (owner, or admin for plain members); cannot_change_owner (403)
  • Platform admin (portal_accounts.platform_role = 'admin'; everyone else gets 404). Admins cannot change, disable or delete their own account; last_admin (409) protects the last active admin.
    • Users
      • GET /api/portal/admin/accounts?search=: { accounts: [{ id, email, displayName, platformRole, verified, disabled, lastLoginAt, createdAt, teamId, teamName, teamRole, verificationEmailStatus }] }
      • POST /api/portal/admin/accounts { email, displayName, teamId?, teamRole?: "admin" | "member", platformRole?: "user" | "admin" }: creates the account with its own new team (or in teamId) and emails a set-password link; returns { account, emailStatus, fallbackLink }; email_in_use / team_full (409)
      • PATCH /api/portal/admin/accounts/{id} { platformRole?, disabled?, displayName?, email?, verified?: true }; email_in_use (409)
      • DELETE /api/portal/admin/accounts/{id}: ownership_transfer_required (409) for owners of teams with other members; deleting the last member deletes the team and retires its cell
      • POST /api/portal/admin/accounts/{id}/password-link: emails a set-password link (never signed in) or reset link; returns { emailStatus, fallbackLink }
      • POST /api/portal/admin/accounts/{id}/verification-link: issues a fresh unsent verification link for an unverified account
    • Teams
      • GET /api/portal/admin/teams?search=: { teams: [{ id, name, status, cellStatus, maxMembers, memberLimit, ownerEmail, memberCount, createdAt, cellId, cellName }] }
      • PATCH /api/portal/admin/teams/{id} { name?, maxMembers?: number | null, suspended?: boolean, cellId?: string | null }: suspended is the kill switch (team routed to its account page only, cell LiteLLM key blocked); cellId assigns (team becomes ready) or unassigns (pending), cell_in_use (409) when the cell serves another team, cell_bound_to_other_team (409) when it ever served another team
      • GET /api/portal/admin/teams/{id}/members, POST … { email, role } (existing account without a team)
      • PATCH /api/portal/admin/teams/{id}/members/{accountId} { role: "owner" | "admin" | "member" } (owner transfers ownership), DELETE … (not the owner)
    • Invite codes
      • GET|POST /api/portal/admin/access-codes: list, or create { count (1–50), maxUses, expiresAt?, note? }
      • PATCH /api/portal/admin/access-codes/{id} { disabled }
    • Cells
      • GET|POST /api/portal/admin/cells: list (with the assigned team and boundTeamId), or register { name, upstreamUrl, publicHost? }; cell_in_use (409) for duplicate names/hosts
      • PATCH /api/portal/admin/cells/{id} { upstreamUrl?, publicHost?, status?: "active" | "disabled" }: status changes block or unblock the cell's model key; retired cells cannot be re-enabled (cell_bound_to_other_team)
      • GET /api/portal/admin/cells/{id}/env: { env } with the cell's CLAPILOT_AUTH_MODE, CLAPILOT_CELL_ID, CLAPILOT_PORTAL_IDENTITY_SECRET, CLAPILOT_PORTAL_URL lines (contains the cell secret)
      • GET /api/portal/admin/cells/{id}/usage: { usage: { configured, spendUsd?, budgetUsd? } } from the cell's LiteLLM key
    • Configuration
      • GET /api/portal/admin/config: { config: { general: { publicBaseUrl, registrationMode, defaultMaxTeamMembers, warmPoolSize }, email: { address, smtpHost, smtpPort, smtpUser, passwordSet }, hub: { url, serviceKeySet }, llm: { baseUrl, masterKeySet, models, defaultModel, budgetUsd, budgetDuration, media } } }; media is { image?, video?, tts?, stt?, realtime?: { models, defaultModel } }, only capabilities with at least one model
      • PATCH /api/portal/admin/config { general?, email?: { address?, smtpHost?, smtpPort?, smtpUser?, password? }, hub?: { url?, serviceKey? }, llm?: { baseUrl?, masterKey?, models?, defaultModel?, budgetUsd?, budgetDuration?, media? } }: secrets are write-only (omitted keeps, "" clears); an empty smtpUser signs in with the sender address; media replaces all media models when present ({} clears them), omitted keeps them; returns the new config
      • POST /api/portal/admin/config/test { target: "email", to? } → { ok, emailStatus }; { target: "hub" } → { ok } or hub_unreachable (502)
      • GET /api/portal/admin/config/llm-models: { models, modes } from the gateway catalog; modes maps model names to the gateway's model kind from LiteLLM /model/info (image_generation, video_generation, audio_speech, audio_transcription, realtime, chat, …), {} when the gateway has no model info; provider_unreachable (502)
  • Hosted cells → portal
    • POST /api/portal/cell-sync: headers x-clapilot-cell-id, x-clapilot-cell-timestamp, x-clapilot-cell-signature (HMAC with the cell identity secret, ±300 s); returns { provider: { label, baseUrl, apiKey, models, defaultModel, media } | null, suspended } (media as in the admin config, empty {} when none are configured); 401 for bad signatures
  • Fleet hub, hosted cells (hub mode, Authorization: Bearer <portal service key>; 404 when no key was generated, 401 for wrong keys)
    • GET /api/hub/fleet/hosted-cells: { ok: true } (connection test)
    • POST /api/hub/fleet/hosted-cells { overrides }: creates a hosted_cell Fleet instance; returns { cell: { instanceId, name, project, status, statusDetail, upstreamUrl } }
    • GET /api/hub/fleet/hosted-cells/{instanceId}, DELETE /api/hub/fleet/hosted-cells/{instanceId}
    • PUT /api/hub/fleet/hosted-cells/settings { egressAllow: ["ip:port"] }: firewall exceptions sent to connectors in the heartbeat response (hosted.egressAllow)
  • Fleet hub admin: GET /api/hub/fleet/hosted-settings → { settings: { serviceKeySet, egressAllow, pullPolicy } }; POST … { action?: "rotate_key" | "revoke_key", pullPolicy?: "always" | "missing" } (rotate_key returns serviceKey once)
  • Hosted cells (CLAPILOT_AUTH_MODE=portal): /api/admin/users GET adds managedByPortal: true; POST/PATCH/DELETE answer 409 { code: "managedByPortal" }. /api/auth/login and /api/auth/password-setup* answer 404. POST /api/auth/update saves only data.display_name; email or password answer 403 with a localized error.
  • Hosted router (same origin as the portal)
    • POST /api/auth/login { email, password }: portal sign-in answered in the instance shape { status: "signed_in", workspace: "ready" | "pending" | "suspended", user } with the session cookies; wrong credentials keep the portal 401 { code: "invalid_credentials" }
    • GET /api/v1/{modules,skills,widgets,agents}[/…] on the edge alias clapilot-hosted-router (cells only): the Fleet hub's read-only catalog, forwarded without cookies; 503 while no hub URL is configured
    • In hosted cells, POST /api/{module,skill,widget,agent}-store/publish answers 403 with a localized error
    • Signed-in workspace API calls while the team has no ready workspace: 409 { code: "workspace_pending" | "workspace_suspended", error }

Voice devices

The Meta Ray-Ban Display web client uses the same device contract with device_kind: meta_rayban_display: pairing, bearer-authenticated heartbeat/history, text turns through POST /api/chat, streamed clapilot.audio attachments and target-aware reply-audio lookup. Voice entry is provided by the glasses system composer; continuous live audio is not implemented in this web client. Pairing and revocation remain owner settings actions, with no additional agent tool. See Voice Devices.

Registry, pairing, device authentication and per-device settings (turn target, conversation mode, live voice) for physical smart speakers bound to one user on one instance (first hardware target: M5Stack Atom VoiceS3R; firmware in firmware/clapilot-voice/). Voice devices are personal-first: every user-facing route operates only on the signed-in user's own devices. Feature page: Voice Devices.

User-facing routes use the normal clapilot_session cookie. Errors are { error } localized via the request UI language with 401, 400, or 404.

  • /api/voice-devices
    • GET: list the signed-in user's devices, newest first, as { devices: VoiceDevice[] }; every device carries its turn_target, voice_mode, and live_voice
  • /api/voice-devices/options
    • GET: everything the settings UIs need to configure a device in one request, as { targets: VoiceDeviceTurnTargetOption[], modes: { id: "push_to_talk" | "live", label, description }[], voices: { id, label }[] }. targets is the same list as /api/voice-devices/targets; modes carries the localized mode names and helper text (ui_language=de|en|it or the usual language resolution); voices lists the 14 GPT-Live relay voices (marin, cedar, quartz, ripple, vesper, willow, stone, gleam, meridian, bossa, tempo, beacon, delta, cinder) with capitalized labels. The web and Apple settings UIs use this route instead of /targets
  • /api/voice-devices/targets
    • GET: list the turn targets the signed-in user may assign to a device as { options: VoiceDeviceTurnTargetOption[] }, in the order device session, main session, team rooms (channels and groups the user is a member of plus public channels; no human DMs, agent DMs, or archived rooms), enabled specialized agents. label and description are localized via the request UI language (ui_language=de|en|it or the usual language resolution), e.g. Teamchat · #general, Agent · Max
  • /api/voice-devices/pairing-codes
    • POST: create a single-use pairing code and return { code: "ABCD-EFGH", expires_at, expires_in_seconds: 600 }. Codes are eight characters from ABCDEFGHJKLMNPQRSTUVWXYZ23456789, displayed as XXXX-XXXX, hashed at rest, and valid for ten minutes; generating a new code invalidates the user's previous unconsumed codes
  • /api/voice-devices/[id]
    • PATCH: rename with { name } (1–80 characters), change routing with { turn_target: VoiceDeviceTurnTarget }, and/or change the conversation settings with { voice_mode: "push_to_talk" | "live" } and { live_voice: string | null } (any combination of the four fields); returns { device: VoiceDevice }. A body with none of the fields returns 400. The target is normalized (roomId/room_id, agentId/agent_id are accepted; malformed input becomes device_session) and validated against the user's access: a room the user is not a member of (unless it is a public channel), a human DM, an archived room, an agent-dm: room, or a disabled/unknown specialist returns 400 with a localized { error }. voice_mode outside the two modes and live_voice outside the relay voice list (case-insensitive; null resets to the server default) also return 400 with a localized { error }. Fields are applied in the order name, target, voice settings; the first invalid field aborts the request, so earlier fields may already be saved
    • DELETE: unpair (hard delete) and return { ok: true }; the device's chat session stays in the user's chat list and the device receives 401 from then on
type VoiceDevice = {
  id: string;
  name: string;
  device_kind: string;            // 'm5stack_atom_voices3r' | 'm5stack_cardputer' | 'm5stack_cardputer_adv' (unknown/missing on pairing -> default)
  hardware_id: string | null;     // e.g. MAC address reported by the device
  firmware_version: string | null;
  token_prefix: string;           // e.g. "clpd_AbCdEf1" (first 12 chars, display only)
  chat_session_id: string | null; // /chat?session=<id> is not guaranteed; show as text/id
  last_seen_at: string | null;
  last_seen_ip: string | null;
  last_status: Record<string, unknown>; // free-form from heartbeat (rssi, ip, uptime_s, ...)
  turn_target: VoiceDeviceTurnTarget; // where push-to-talk turns go, default { kind: "device_session" }
  voice_mode: "push_to_talk" | "live"; // conversation mode, default "push_to_talk"
  live_voice: string | null;      // relay voice for live mode; null = server default ("marin")
  created_at: string;
};

type VoiceDeviceTurnTarget =
  | { kind: "device_session" }                     // the device's own chat session (default)
  | { kind: "main_session" }                       // the owner's main personal chat
  | { kind: "team_chat"; roomId: string }          // posted as the owner into a team room; main agent replies
  | { kind: "specialized_agent"; agentId: string }; // direct conversation with a specialist (device session pinned to it)

type VoiceDeviceTurnTargetOption = {
  id: string;          // "device_session" | "main_session" | "team_chat:<roomId>" | "specialized_agent:<agentId>"
  kind: VoiceDeviceTurnTarget["kind"];
  label: string;       // server-localized
  description: string; // server-localized helper text
  target: VoiceDeviceTurnTarget;
};

The selected option id for a device is turn_target.kind for the two simple kinds, or team_chat:<roomId> / specialized_agent:<agentId>. Routing semantics per kind are described on the feature page under Turn targets; the target applies to push-to-talk turns only, live-mode conversations run through the personal relay (see Conversation modes).

Device-facing routes:

  • /api/voice-devices/pair
    • POST (public, no cookie, rate limited): body { code, hardware_id?, device_kind?, firmware_version?, name? }. The code is normalised (uppercase, strip everything that is not A-Z/2-9) before lookup
    • 200: { device: { id, name, device_kind }, device_token: "clpd_...", chat_session_id, instance_url, user: { display_name }, api: { chat: "/api/chat", live_relay_session: "/api/chat/live/relay/session", live_tools: "/api/chat/live/tools", heartbeat: "/api/voice-devices/heartbeat" } }. device_token (clpd_<base64url 32 bytes>) is returned exactly once and stored as a SHA-256 hash
    • 400: { error, code: "invalid_code" } for unknown, expired, and already-used codes alike (no distinction is leaked)
    • 429 with Retry-After when throttled
  • /api/voice-devices/heartbeat
    • POST (device bearer auth): body { firmware_version?, status?: object }; updates last_seen_at, last_seen_ip, firmware_version, and last_status, and returns { ok: true, device: { id, name, device_kind }, chat_session_id, voice_mode, live_voice, turn_target_kind, server_time }. voice_mode, live_voice (null = server default) and turn_target_kind (turn_target.kind) are the owner's current device settings; devices apply them on their next button press, so settings changes propagate with the heartbeat interval
  • /api/voice-devices/me
    • GET (device bearer auth): same response shape as heartbeat, without writes
  • /api/voice-devices/reply-audio
    • GET (device bearer auth): ?since=<ISO 8601> (default: five minutes ago; an unparsable value returns 400). Returns { audio: attachment | null, text, pending } for the newest assistant message created at or after since in the device's target (its device session, the owner's main session, or the configured team room), preferring a message that already carries reply audio. audio is the same attachment shape as the clapilot.audio stream frame (id, url, mimeType, durationMs?, transcript?), text is the reply text, and pending: true means the reply is still being generated. Devices use it as the fallback when the chat stream carried no clapilot.audio frame, in particular for asynchronous specialist replies

Device bearer auth (Authorization: Bearer clpd_...) is accepted only on the allowlist listed under Auth behavior. A device-authenticated POST /api/chat is routed by the device's stored turn_target, never by routing fields in the request body: device_session (default) uses the device's own chat session (titled after the device) so the conversation appears in the owner's chat list, main_session uses the owner's main chat, team_chat posts the transcript as the owner into the room (roomId from the target; the main agent reply is forced for the speaker unless a specialist is explicitly mentioned), and specialized_agent pins the device session to that specialist. Push-to-talk turns send a type: "audio" attachment as documented under Chat and calendar. In live mode (voice_mode: "live") the device instead opens POST /api/chat/live/relay/session with its token, the configured live_voice as voice, and clientContext.pageContext.scope = "voice-device", streams 24 kHz PCM16 input_audio_buffer.append events to the relay input route, plays response.output_audio.delta from the relay event stream, executes model function calls through POST /api/chat/live/tools with its chat_session_id as sessionId, and closes with DELETE; the relay and live tool routes are reused unchanged and the turn target does not apply to relay sessions. Pairing and unpairing are intentionally not exposed as chat/live agent tools.

Voice-device chat and audio specifics:

  • POST /api/chat accepts, besides the usual audio formats, a type: "audio" attachment with mimeType: "audio/pcm" (also audio/l16, audio/x-pcm): raw PCM16 mono little-endian at 24 kHz. The server wraps it into a WAV (name gets a .wav extension, durationMs is computed from the byte length) before STT and storage, so streaming clients that do not know the length up front can send it with chunked transfer encoding.
  • POST /api/chat authenticated with a device token behaves like the Watch walkie-talkie turn. When the request carried an audio attachment, the server synthesizes the TTS reply before terminating the stream and emits one extra SSE frame data: {"type":"clapilot.audio","attachment":{"id","type":"audio","name","size","mimeType","url","durationMs?","transcript?"}} right before data: [DONE]. Browser and Apple clients are unaffected: for them the reply audio is still persisted after the stream ends.
  • Reply audio for team-room turns is stored like personal reply audio: chat_audio_assets rows may reference a team chat message through group_message_id instead of chat_message_id (migration 320_voice_device_turn_targets.sql; chat_message_id is nullable and a check constraint requires at least one of the two). Specialist replies to device turns are synthesized when the specialist turn completes and are then discoverable through GET /api/voice-devices/reply-audio.
  • GET /api/chat/audio/[id]?format=pcm16&sample_rate=16000 returns the stored reply transcoded to raw little-endian PCM16 mono (Content-Type: audio/L16; rate=<n>; channels=1, whole file, no Range support) so a speaker can stream it straight into I2S without an MP3 decoder. Supported rates: 8000, 11025, 16000, 22050, 24000, 32000, 44100, 48000 (default 16000). Unknown formats or rates return 400. Without format the route streams the stored file as before.

Onboarding

  • /api/onboarding
    • GET: return whether the first-login onboarding flow is enabled plus the signed-in user's persisted onboarding state (status, currentStep, timestamps, and optional templateSetup marker).
    • POST: advance, complete, skip, or restart the flow with { action, currentStep?, ui_language? }. Progress is stored in user_profiles.onboarding_state_json.
  • /api/onboarding/templates
    • POST: queue background personalized-template generation from the optional onboarding step with { document_ids, ui_language? }. Validates that the documents are visible to the signed-in user, caps the batch at 10, and enqueues a one-off, user-owned agent task (via createScheduledTask) that reads each document and authors a reusable Canvas template. The agent's final reply is delivered to the user (chat + push); the call also marks onboarding_state_json.templateSetup as queued.

Documents

Untrusted document responses

Document previews, downloads and public shares use one response policy: known passive PDF/image/audio/video/plain-text formats retain inline preview and range support; active HTML/SVG and unknown types download as attachments with a restrictive opaque-origin CSP sandbox. Every document response disables MIME sniffing.

  • /api/documents
    • GET: list document metadata lazily for the Dokumente UI, scoped to shared documents plus the signed-in user's private documents, including filename suggestion metadata (original_file_name, suggested_file_name, auto_renamed), extracted tax fields (amount_cents, currency, datum), and related_mandanten. Supports limit, offset, search, mandant_id, kategorie, folder_view (all, none, google-drive, microsoft-365, folder), folder_id, sort_field, sort_direction, and optional include_facets=1 for sidebar counts. mandant_id matches the primary dokumente.mandant_id or any document_mandanten relation. document_id returns metadata for one record so deep-linked previews can open without loading the full archive.
  • /api/documents/upload
    • POST: accepts multipart file, typ, and titel plus optional document metadata including mandant_id, datum, amounts, and folder_id. When supplied, folder_id must be a valid UUID for an existing document_folders row; invalid or missing folder targets return 400, and the inserted dokumente row stores the folder reference.
  • /api/documents/inbox
    • Document-like uploads in /api/chat share the Documents format catalog and 100 MB limit. They are persisted as shared records under relative _inbox/chat-uploads/... paths before execution (clients upload them first via POST /api/chat/uploads and reference them by documentId), synchronously extracted when supported, added to the current agent context, and returned by the agent as title/link references. Unsupported chat formats return the concrete supported list and point users to Dokumente → Hochladen.
    • POST: accepts multipart files plus optional folder_id, repeated directories entries to recreate dropped folder trees in documents, and AI-processing controls for large batches: processing_mode (full, index_only, sample), processing_confirmed=1 for confirmed full processing, and optional processing_limit for sample batches. Large full-processing uploads return 409 with requiresProcessingConfirmation until explicitly confirmed.
  • /api/documents/folders
    • GET: list folder tree rows visible to the signed-in user, including the per-user private Persönlich folder
    • POST: create a folder with { name, parent_id?, visibility_scope? }; omitted scope creates a shared folder
  • /api/documents/folders/[id]
    • PATCH: rename or re-parent a folder with { name?, parent_id? }
    • DELETE: delete a folder and lift contained documents/subfolders one level up
  • /api/documents/[id]
    • GET: download the stored document file as an attachment
    • PATCH: update document metadata with any subset of { folder_id, mandant_id, typ, titel, beschreibung, datum, amount_cents|amount, currency, quartal, jahr, kategorie, related_mandant_ids }. related_mandant_ids replaces the non-primary document relations while preserving the primary mandant_id relation.
    • PUT: replace the stored file bytes for an existing document and refresh document indexing metadata (max 100 MB). PDF documents keep the strict guard (request Content-Type must be application/pdf and the body must carry the %PDF- magic bytes); non-PDF documents accept a raw body whose Content-Type matches the stored mime type (application/octet-stream bypasses the mime check). Used by the native Apple iPad signing flow (PencilKit annotations) and by the macOS Documents folder sync to push local file edits
    • DELETE: delete the document record and its stored file
  • /api/documents/[id]/export-pdf
    • POST: export a Word Editor-compatible document as A4 PDF, save the generated PDF as a sibling document record in Clapilot, and return the created document metadata plus a download URL
  • /api/documents/[id]/preview
  • /api/documents/[id]/thumbnail
    • GET: authenticated PNG thumbnail of the first PDF page (rendered via pdftoppm, cached for 300s); used by document list/grid tiles
  • /api/documents/[id]/analysis
    • GET: return AI-side document extraction status plus extracted preview text, extraction metadata, generated bullet summary, and filename suggestion metadata for the document preview sidebar; the route also ensures a background index job exists when the document has a file path
  • /api/documents/[id]/workflow
    • GET: ensure + return the document workflow { automation }. Once extraction status is processed, Clapilot analyzes the extracted text and AUTO-EXECUTES the detected actions (no separate approval step): it returns a 1-2 sentence summary, context_label, document_kind, matched mandant, an array task_actions (a single document can yield several tasks — e.g. one per next step in a meeting summary), a calendar_action, a follow_up_action, and highlights. Each created/updated task carries task_id and action_kind (created or updated). Tasks are created in aufgaben with source_type='document' (linking the document back into each task); calendar entries use source_origin='document'. Before creating, a suggested task is matched against existing open tasks for the same client and tasks already linked to the document — a strong title match UPDATES that task (repointing a manual task to the document, or appending the document link to an email-sourced task) instead of creating a duplicate. The analysis + execution is cached per document via a content signature, so repeated reads do not re-run the model or re-create tasks; it also emits an aufgaben UI mutation reload when tasks are created/updated. automation is null until extraction is processed. The document detail view (web + Apple) renders this as the "Workflow" timeline section. There is no POST approval route — actions are applied automatically.

beA

  • /api/bea/import
    • POST: authenticated multipart upload for one beA export ZIP via file or the first files entry. Requires the admin feature flag app_settings.bea_import_enabled; it is disabled by default. The route parses XML metadata and attachments, stores files under _inbox/bea, upserts bea_nachrichten by nachrichten_id, upserts imported attachment rows in dokumente with source_type='bea', enqueues document indexing, and starts a native background run with sessionKey=system:bea-inbox:process.

Admin terminal

  • /api/admin/terminal/sessions
    • POST: admin-only and requires app_settings.developer_mode_enabled=true plus the bundled terminal module to be active. Creates a short-lived node-pty Bash session in the Clapilot web container and returns { sessionId, cwd, shell, cols, rows, pid }.
  • /api/admin/terminal/sessions/[id]/stream
    • GET: admin-only SSE stream for terminal output events. The stream sends JSON messages with type=ready|output|exit|error.
  • /api/admin/terminal/sessions/[id]/input
    • POST: admin-only input write for an existing terminal session with { data }. Input chunks are capped server-side.
  • /api/admin/terminal/sessions/[id]/resize
    • POST: admin-only PTY resize with { cols, rows }.
  • /api/admin/terminal/sessions/[id]
    • DELETE: admin-only close for an existing terminal session. Idle and max-age cleanup also close forgotten sessions.

Terminal UI is rendered by the bundled module at /modules/terminal; module discovery hides it while Developer mode is disabled. These endpoints remain first-party app APIs because they manage process-local PTY sessions. Sessions are not resumable after container restart. Session lifecycle events are recorded in admin_terminal_audit_events without storing terminal input/output content.

Wiki

  • /api/wiki/pages
    • GET: list active Wiki pages visible to the signed-in user or a user-scoped agent system token. Supports search, limit, offset, and include_archived=true; searches title, summary, Markdown content, and tags.
    • POST: create a manual Wiki page with { title, slug?, markdown_content?, summary?, tags? }; creates immutable revision 1 and returns { page }.
  • /api/wiki/pages/[id]
    • GET: load one Wiki page by UUID, slug, or stable topic_key, including provenance/evidence metadata and revision/proposal counts.
    • PATCH: make a manual edit with any subset of { title, slug, markdown_content, summary, tags, status, change_summary }; appends an immutable revision. The client cannot relabel the edit as generated or replace provenance fields.
    • DELETE: archive one Wiki page and append an archive revision; returns { page, archived: true }.
  • /api/wiki/pages/[id]/revisions
    • GET: list immutable page revisions newest first; supports limit and offset.
  • /api/wiki/pages/[id]/revisions/[revisionId]/revert
    • POST: admin-only. Restores a historical snapshot as a new manually owned revision; the historical row is never changed.
  • /api/wiki/proposals
    • GET: list proposals; supports page_id, comma-separated state, limit, and offset. The response includes can_review for the signed-in admin UI.
    • POST: create a generated draft with { target_page_id?, topic_key?, slug?, title, markdown_content, summary?, tags?, source_type?, source_refs, assertion_refs, evidence_refs, evidence_checked_at?, metadata?, proposed_by_ref? }. Assertion/evidence refs are exact objects containing at least { kind, id }. Exact duplicates or semantic candidates at/above the fixed wiki-semantic-v1 Jaccard threshold (0.78) return 409; the signature uses title, summary, and H1-H3 headings and is identical in the web helper, native publisher, and migration backfill.
  • /api/wiki/proposals/[id]
    • GET: read one proposal.
    • PATCH: admin-only review with { decision: "approved" | "rejected", review_note? }. Approval does not mutate the live page. Approval rejects private, terminal, conflicted, unresolved-conflict, or evidence-pending canonical assertions. Candidate assertions promoted by this review enqueue projection work under each assertion's real instance_key.
  • /api/wiki/proposals/[id]/publish
    • POST: admin-only. Atomically publishes an approved, non-stale proposal, appends its immutable revision, and marks the proposal published.

Wiki read/manual-write and proposal-create APIs accept normal signed-in user cookies and short-lived Bearer tokens minted by /api/auth/agent/system-token with modules:api:read or modules:api:write. Review, publish, and revert require a signed-in admin. Generated producers should create proposals instead of calling the manual page mutation route; manually protected pages reject unreviewed generated writes at both helper and database boundaries. Authenticated chat/Live Voice wiki_upsert_page calls are explicitly user-directed: the tool proxy sends source_type=manual and metadata.userDirected=true, producing a protected manual revision. Autonomous Dreaming remains proposal-only.

Notes and dictation

  • /api/notizen-audio
    • POST: authenticated multipart upload for a note page voice attachment; requires file, noteId, and pageId, accepts optional transcript, durationMs, contentText, and contentHtml, stores the audio bytes, and returns { attachment, transcriptText }
  • /api/notizen-audio/[id]
    • GET: authenticated download/stream endpoint for one stored note voice attachment owned by the signed-in user
  • /api/modules/notizen/api/notes/[id]/duplicate-local
    • POST: clone a read-only source note into a normal editable local note; returns the new { note, pages, assets?, duplicated_from_note_id } payload used by web, Apple, and agent flows
  • Notizen note/folder/page payloads now also expose source metadata for synced imports:
    • notes: source_kind, source_external_id, source_synced_at, is_read_only, source_metadata
    • folders: system_key, is_read_only
    • pages: source_external_id, source_metadata
  • read-only source notes reject direct note/page mutation and note-audio uploads; the supported write path is the duplicate-local endpoint above

Bundled module APIs

  • /api/modules/canvas/api/files
    • GET: list recent signed-in-user Canvas .html files recursively from .clapilotaicore/canvas, including shared files and each durable revision, is_new, and last_opened_at, sorted newest-first, with optional limit, search, folder, new_only (1, true, yes), and inclusive ISO date-time since (invalid values ignored); response new_count counts new files after filtering and before the limit
    • POST: create a new Canvas file with { title?, path?, folder?, content_html? }; omitted path generates a unique .html filename and creates missing subfolders as needed
  • POST /api/modules/canvas/api/opened: { path, shared? } records the signed-in user’s opened timestamp for an existing file; returns { ok, path, shared, last_opened_at, is_new: false }. Missing files return 404; other methods return 405.
  • /api/modules/canvas/api/files/[...path]
    • Add ?shared=1 to read or update a shared Canvas file in the instance-wide shared scope. Shared files are collaborator-writable; delete remains owner-only.
    • GET: load one Canvas file and return { file, content_html }; file.revision is the durable mutation revision
    • PUT/PATCH/POST: replace one Canvas file with { title?, content_html|contentHtml|html, idempotency_key?, expected_revision? }. With an idempotency key, Canvas journals the intended revision and content hash before the atomic file replacement; replay after interruption returns the same revision with mutation_replayed=true. A stale expected_revision or reuse of a key for different content returns 409.
    • DELETE: delete one Canvas file
  • /api/modules/canvas/api/folders
    • POST: create a Canvas subfolder with { path }
  • /api/modules/canvas/api/templates
    • GET: list signed-in-user Canvas templates from .clapilotaicore/canvas-templates, with optional limit and search
    • POST: create a template from JSON { name, description?, kind?, source_text?, template_html? } or from multipart upload field file for PDF, Word, Excel, or CSV sources; PDF uploads are converted into image-backed template HTML with placeholder overlays for detected dynamic values, and other supported sources are stored with an extracted preview plus draft placeholder HTML for agent refinement
  • /api/modules/canvas/api/templates/[id]
    • GET: load one Canvas template including { template_html, fields, source_text_preview }
    • PUT/PATCH/POST: update a Canvas template with { name?, description?, kind?, template_html|templateHtml|html?, fields? }
    • DELETE: delete one Canvas template and its stored uploaded source when present
  • /api/modules/canvas/api/templates/[id]/files
    • POST: render a saved template into a new Canvas .html file with { title?, path?, folder?, data? }, replacing {{field}} placeholders from data
  • /api/modules/canvas/api/preview/[...path]
    • GET: serve one Canvas HTML file as a no-store preview response for sandboxed display
  • /api/modules/canvas/api/folders
    • GET: list Canvas folder paths, including empty folders
    • POST: create a Canvas folder with { path }
  • /api/modules/canvas/api/folders/[...path]
    • PUT/PATCH: rename a Canvas folder with { path }, moving contained Canvas files under the new folder path
    • DELETE: delete a Canvas folder and its contained Canvas files/subfolders
  • /api/modules/canvas/export-pdf
    • POST: render submitted Canvas HTML (title, optional path, content_html, optional idempotency_key) into a private PDF Documents entry. Optional print controls are page_format (A4, A3, A5, Letter, Legal), orientation (portrait, landscape), and margin (narrow, normal, wide, or 0–50 millimeters; margin_mm is the numeric alias). If none are supplied, an existing document @page rule is preserved; HTML without @page retains the edge-to-edge A4 portrait default. Each explicit print control overrides its matching page descriptor. The response includes the created document metadata, documentUrl, downloadUrl, and best-effort pages. HTML explicitly marked with data-clapilot-document="a4" and .page containers uses the same fixed A4 boxes as the Canvas preview; page overflow or incompatible page geometry returns 422 without creating a document.
    • repeated requests with the same authenticated owner and idempotency_key return the original Documents row with deduped=true; agent calls derive this key from the durable run ID plus normalized export arguments so restart replay cannot create a duplicate PDF mutation
  • /api/modules/canvas/export-html
    • POST: save submitted Canvas HTML (title, optional path, content_html, optional idempotency_key) as a standalone private .html Documents entry (mime_type: text/html). Bare fragments are wrapped into a full HTML document and signed generated-image URLs are inlined as data: URIs so the file works outside Clapilot; scripts and interactivity are preserved. The response includes the created document metadata, documentUrl, and downloadUrl; repeated requests with the same owner and idempotency_key return the original row with deduped=true
  • /api/modules/canvas/public-share
    • GET: read the active public share for a canvas file (query path, optional shared=1 for instance-shared files); returns share with its public url or null
    • POST: create — or return the existing — active public share for a canvas file ({ path, shared?, expires_in?, force_new? }; expiry accepts 24h, 7d, permanent, or an ISO timestamp; force_new rotates the link). The share is stored in canvas_public_shares and points at the file by path, so the link serves the current content
    • DELETE: revoke the active public share (body or query path, shared)
  • /share/canvas/[hash]
    • GET: public, login-free route serving the current HTML content of a publicly shared canvas file; rate-limited, noindex, and served with a sandboxing CSP (opaque script origin). Returns 404 for unknown/revoked links or missing files and 410 for expired links
  • /api/canvas-style
    • GET/PUT/POST: read or update the instance-wide styleguide (brand colors, fonts, heading/body sizes, box/table styling, default logo) stored in canvas_style_settings; the same store backs the canvas_get_style_settings / canvas_update_style_settings agent tools and the Settings → Styleguide page (/settings/styleguide; the old /settings/canvas-style path redirects there)
  • /api/styleguide/assets
    • GET: list the workspace-global styleguide brand assets ({ assets: [{ id, name, kind, mime_type, byte_size, description, workspace_path, url, reference_capable }] }); kind ∈ logo/icon/image/background, reference_capable marks raster assets (PNG/JPG/WEBP) usable as image-generation source images
    • POST: multipart upload (file, optional name, kind, description, max 10 MB; content type is sniffed from magic bytes — PNG, JPG, WEBP, GIF, SVG). Bytes are stored on the shared workspace volume under .clapilot/styleguide-assets/; metadata in the styleguide_assets table. Error codes: unsupported_file_type, file_too_large, migration_missing
  • /api/styleguide/assets/[id]
    • PATCH: update name, kind, or description
    • DELETE: remove the asset row and its workspace file
  • /api/styleguide/assets/[id]/file
    • GET: stream the asset bytes to authenticated users (SVG is served with a no-script CSP)
  • /api/styleguide/styles
    • GET: list the additional named styleguide styles ({ styles: [{ id, name, style, ... }] }); the instance default style stays in canvas_style_settings via /api/canvas-style
    • POST: create a named style ({ name?, style? }; without a style seed the current default is copied). Stored in the styleguide_styles table (migration 297)
  • /api/styleguide/styles/[id]
    • PATCH: update name and/or merge a partial style token patch; { make_default: true } swaps the style's tokens with the instance default so the previous default is preserved in the named slot (returns default_style plus the swapped style)
    • DELETE: remove the named style (the default style is unaffected)
  • /api/modules/cases/api/health
    • GET: module health for the bundled Cases module and schema readiness
  • /api/modules/cases/api/options
    • GET: compact Mandanten and user/lawyer options for case forms
  • /api/modules/cases/api/cases
    • GET: list legal cases/matters with optional q/search, status, mandant_id, assigned_user_id, practice_area, limit, and offset
    • POST: create a legal case with title, optional case_number, status, priority, practice_area, mandant_id, assigned_user_id, court/reference fields, conflict-check fields, dates, and description
  • /api/modules/cases/api/cases/:id
    • GET/PATCH/DELETE: read, update, or delete one legal case
  • /api/modules/cases/api/cases/:id/overview
    • GET: case summary plus party, key-date, communication, and linked-entity counts
  • /api/modules/cases/api/cases/:id/timeline
    • GET: merged case timeline across communication logs, key dates, and linked documents/tasks/calendar/email/note records; supports filter, limit, and offset
  • /api/modules/cases/api/cases/:id/parties
    • GET/POST/PATCH/DELETE: manage case parties such as client, opposing party, opposing counsel, court, witnesses, experts, insurance, and other contacts
  • /api/modules/cases/api/cases/:id/key-dates
    • GET/POST/PATCH/DELETE: manage case deadlines, hearings, filings, limitation dates, appointments, and review dates; rows may reference a linked calendar event
  • /api/modules/cases/api/cases/:id/communications
    • GET/POST: list or add communication log entries for email, phone, meeting, letter, portal, fax, or internal notes
  • /api/modules/cases/api/cases/:id/links
    • GET/POST/DELETE: link or unlink existing document, task, calendar_event, email, draft, or note entities to a case
  • /api/modules/cases/api/linkable
    • GET: search existing link targets with type=documents|tasks|calendar and optional q
  • /api/modules/social-media/api/* (bundled Social Media module; replaces the legacy /api/modules/linkedin/api/* surface)
    • GET health, GET bootstrap (legacy Settings LinkedIn card contract), GET accounts, POST accounts/mastodon, POST accounts/bluesky, DELETE accounts/:platform/:id (legacy DELETE accounts/:id = linkedin)
    • GET/POST posts, GET/PATCH/DELETE posts/:id, POST posts/:id/publish|schedule|unschedule, POST posts/publish-due (worker/service entry, cross-user). POST posts accepts optional idempotencyKey; the first request creates the post and a replay for the same user/key returns it without inserting a duplicate. GET posts accepts status, q, limit (1-200), and a non-negative offset; posts are returned in campaign order (next upcoming first by coalesce(scheduled_for, planned_for), undated drafts next, posted/partial/failed last, then created_at/id), which is deterministic and lets the UI and agent bulk workflow enumerate every reviewable page. Post payloads include plannedFor (editorial planning date); POST posts and PATCH posts/:id accept plannedFor without requiring targets, and null/empty clears it. POST posts/:id/review-action accepts { action, language?, requestId?, expectedWeeklyPrompt? } for draft and ready_for_review posts and claims/completes one guarded, idempotent agent rewrite; the claim atomically rechecks that status, while stale/orphaned in_progress jobs are restored before a replacement claim. For action=regenerate, bulk callers pass the confirmed focus in expectedWeeklyPrompt; generation uses that exact snapshot, then final persistence takes a short FOR UPDATE strategy-row lock and applies only when the stored value still matches. A mismatch discards the model output, restores the claimed post's prior review status, and returns { code: "weekly_focus_changed" } with 409 so the caller aborts the remaining batch. No database lock spans the external model call. Failed/cancelled IDs require a fresh requestId, failures restore the pre-claim status only while that job remains the current active claim, target/text changes cancel late completion, and media-only updates do not conflict. POST posts/:id/media atomically and idempotently appends one validated media item without replacing concurrent draft edits. X publishing preflights required OAuth scopes (including media.write for media), treats a stored prior provider error as advisory so token refresh can self-heal, stores bounded provider status/request/problem diagnostics on a failed target, and is not considered confirmed by the agent tool until every posted target has a remote id; provider URLs are best-effort.
    • POST generate (AI draft { brief, tone?, platforms?, language? } -> { title, content, hashtags[] })
    • POST generate-image-prompt refines { prompt, postId?, title?, content?, platforms?, stylePreset?, allowText?, allowScreenshots?, language?, useBrandReference?, referenceImagePath?, brandAssetId?, styleId? } into one English { prompt, sourceImagePath, brandLogo } using the post content as semantic context (one post-specific visual concept, generic diagram clichés banned, copy never typeset), the workspace strategy and weekly focus, and the resolved styleguide (default Clapilot tokens included, or the named style selected via styleId) as binding palette/geometry/type direction. useBrandReference defaults to true: the brand logo (styleguide logo asset first, otherwise the styleguide logo url materialized under .clapilot/brand-assets/) is returned as brandLogo { path, name, source } and as sourceImagePath, and the prompt places it as a small unaltered corner signature. An attached post image (referenceImagePath) or a styleguide brand asset (brandAssetId, pre-resolved by the app runtime) replaces the logo as the anchor. allowText defaults to false. Native runtime requests fall back to the compatibility gateway, while clients retain a compact non-AI fallback
    • GET brand-reference?styleId= returns { brandLogo: { path, name, source } | null }, the logo reference image the composer attaches by default (used when AI prompt refinement falls back locally)
    • POST suggest-image-description turns { title?, content?, platforms?, stylePreset?, styleId?, language? } into one short, user-editable { description } in the requested UI language for the image dialog's Bildbeschreibung field. The suggestion names a concrete subject/composition based on the post, strategy, weekly focus, style preset, and (when styleId selects a named styleguide style) that style's tokens — without restating palette/typography, which the prompt builder applies later. Same native-runtime → gateway fallback as generate-image-prompt
    • GET image-models returns the configured app_settings.media_model_catalog.image entries for per-generation model selection
    • GET media, POST media/import (kind: upload | video-studio | livestream-asset), GET media/file?path=, DELETE media?path=
    • GET|POST oauth/start?platform=linkedin|youtube&accountType=&returnTo= and GET oauth/complete; oauth/complete is the only public (unauthenticated) endpoint of this module, allowlisted in src/app/api/modules/[slug]/api/[...endpointPath]/route.ts
    • details: Social Media
  • /api/modules/image-playground/api/* (bundled Image Playground module)
    • GET/PUT/POST/DELETE state: personal playground state stored as per-user JSON under <workspace>/.clapilotaicore/module-state/image-playground/ (never inside the module folder, which is replaced with every image update), with an 80-version cap; PUT and POST both replace the state
    • GET health
    • details: Image Playground
  • /api/modules/whiteboard/api/* (bundled Whiteboard module; boards are visible to their owner plus, when the owner enables team visibility, to every signed-in user — delete/unshare stay owner-only)
    • GET health
    • GET/POST boards, GET/PATCH/DELETE boards/:id, POST boards/:id/duplicate
    • POST boards/:id/items, PATCH/DELETE boards/:id/items/:itemId
    • full-scene PATCH writes validate item ids and reject scenes larger than about 15 MB; agent item creation assigns missing ids/z-order and auto-places omitted coordinates
    • details: Whiteboard
  • /api/modules/shopify/api/* (bundled Shopify module; store connections are workspace-global; authenticated like all module handler APIs — session cookie or agent service auth; every endpoint returns {"error":"module_not_installed"} while the module is disabled)
    • GET stores — connected stores, access tokens and client secrets never included; POST stores { shop_domain, client_id, client_secret, label? } (Dev Dashboard client-credentials grant; the connect UI only offers this method) or { shop_domain, access_token, label? } (admin token — API-only legacy fallback for pre-existing admin custom apps) — exactly one auth method per request (both or neither → 422 shopify_auth_failed); verifies the credentials live via the GraphQL Admin API shop query (client credentials first mint a short-lived token) before saving; POST stores/:id/verify; DELETE stores/:id — store objects from GET stores, POST stores, and POST stores/:id/verify include auth_method: "access_token" | "client_credentials"
    • GET stores/:id/overview — store overview dashboard
    • GET stores/:id/products?search&status&cursor&page_size; GET stores/:id/products/:productId — product detail now includes vendor, tags, and images as [{ id, url, alt_text }]; POST stores/:id/products { title, description_html?, status? (active|draft|archived, default draft), vendor?, tags?, price?, sku? } → 201 { product }; POST stores/:id/products/:productId { title?, description_html?, status?, vendor?, tags? }; DELETE stores/:id/products/:productId → { ok, deleted_product_id }
    • POST stores/:id/products/:productId/variants { option_values (positional string array), price?, compare_at_price?, sku? } → 201 { variant }; POST stores/:id/products/:productId/variants/:variantId — at least one of { price, compare_at_price, sku, barcode } → { variant }; DELETE stores/:id/products/:productId/variants/:variantId → { ok }; POST stores/:id/products/:productId/variants/:variantId/price { price, compare_at_price? }
    • POST stores/:id/products/:productId/images { image_url, alt_text? } → 201 { ok, media_id, media_status } (Shopify processes the media asynchronously); DELETE stores/:id/products/:productId/images/:mediaId → { ok }
    • GET stores/:id/locations; POST stores/:id/inventory/adjust { inventory_item_id, location_id, delta, reason? }; POST stores/:id/inventory/set { inventory_item_id, location_id, quantity, reason? } → { ok, new_quantity } (absolute available quantity)
    • GET stores/:id/orders?status&cursor (open/closed/cancelled); GET stores/:id/orders/:orderId — order detail with line items and fulfillments
    • GET stores/:id/customers?search&cursor
    • errors: 422 shopify_auth_failed, 400 invalid_domain, 404 store_not_found, 429 shopify_rate_limited
    • details: Shopify
  • /api/modules/home-assistant/api/* (bundled Home Assistant module; connections, dashboard tiles, voice targets, and automations are workspace-global; authenticated like all module handler APIs — session cookie or agent service auth; every endpoint returns {"error":"module_not_installed"} while the module is disabled; connection create/update/delete and the connection test are admin-only → 403 forbidden otherwise (POST connections/:id/verify with the stored token is open to every authenticated user); list endpoints accept optional connection_id and fall back to the default connection, 409 no_connection when none exists; the handler talks to the Home Assistant REST API server-side with the stored long-lived token, 15 s timeout)
    • GET connections → { connections: [{ id, name, base_url, is_default, location_name, ha_version, last_verified_at, last_error, has_token, created_at, updated_at }] } — tokens never included; POST connections (admin) { name, base_url, token, is_default? } — verifies live via GET /api/config before saving, first connection becomes default → { connection }; PATCH connections/:id (admin) { name?, base_url?, token?, is_default? } — re-verifies when base_url or token change → { connection }; POST connections/:id/verify → { connection, ok: true } (updates location_name/ha_version/last_verified_at, or stores last_error and answers 422/502); DELETE connections/:id (admin) → { ok: true } (cascades tiles and voice targets; promotes the oldest remaining connection to default); POST connections/test (admin) { base_url, token } — verify without saving → { ok, location_name, ha_version }
    • base URL rules: http: or https:, host required, no credentials, no path/query, trailing slashes stripped; LAN/private hosts allowed → 400 invalid_base_url otherwise
    • GET entities?connection_id&search&domain&limit → { connection_id, entities: [{ entity_id, domain, name, state, unit, device_class, attributes (trimmed: max 40 keys, values > 500 chars dropped), last_changed, suggested_tile_kind, on_dashboard }], total } (sorted by name; limit default 200, max 1000); GET entities/domains?connection_id → { domains: [{ domain, count }] }; GET entities/:entity_id?connection_id → { entity } with full attributes
    • POST services/call { connection_id?, domain, service, entity_id?, data? } → { ok: true, changed: [states…] } — domain/service must match ^[a-z_][a-z0-9_]*$ (400 invalid_service), entity_id must match ^[a-z_][a-z0-9_]*\.[a-z0-9_]+$ (400 invalid_entity_id), data must be a flat object with at most 40 keys
    • GET camera/:entity_id/snapshot?connection_id → binary image response (content type from Home Assistant, cache-control: no-store); 404 when the entity is not a camera
    • GET dashboard?connection_id → { connection_id, tiles: [{ id, connection_id, entity_id, tile_kind, title, size, position, config, state: { state, attributes, last_changed } | null }] } — states merged from one GET /api/states call; when Home Assistant is unreachable tiles come back with state: null plus warning: "home_assistant_unreachable"
    • POST dashboard/tiles { connection_id?, entity_id, tile_kind?, title?, size?, config? } → { tile } (tile_kind derived from the live entity's domain when omitted: camera | toggle | cover | lock | climate | media_player | sensor | state; 409 tile_exists on duplicate); PATCH dashboard/tiles/:id { title?, size?, tile_kind?, config?, position? } → { tile }; size is a grid span: small (1×1) | wide (2×1) | tall (1×2) | large (2×2) | WxH with W = 1–4 columns and H = 1–3 rows, canonicalized to the alias when one matches; tiles serialize size plus integer width/height; POST dashboard/tiles/:id/move { direction: "up" | "down" } or { index } (0-based target slot, clamped to the end; positions are renumbered contiguously) → the full dashboard payload ({ connection_id, tiles, warning? }); DELETE dashboard/tiles/:id → { ok: true }
    • POST dashboard/tiles/:id/action { action, data? } → { ok, tile_id, entity_id, state } — maps to service calls by tile domain: toggle domains turn_on | turn_off | toggle; scene turn_on | activate; cover open | close | toggle | stop | set_position (data.position 0–100) | set_tilt_position (alias set_cover_tilt_position, data.tilt_position 0–100) | open_tilt | close_tilt; lock lock | unlock | open; light turn_on | turn_off | toggle | set_brightness (data.brightness_pct 0–100, sent as light.turn_on) | set_color_temp (data.color_temp_kelvin 1000–10000, sent as light.turn_on); fan turn_on | turn_off | toggle | set_percentage (data.percentage 0–100); humidifier turn_on | turn_off | toggle | set_humidity (data.humidity 0–100); climate set_temperature (data.temperature) | set_hvac_mode (data.hvac_mode) | set_fan_mode (data.fan_mode) | turn_on | turn_off | toggle; media player media_play | media_pause | media_play_pause | media_stop | media_next_track | media_previous_track | volume_up | volume_down | volume_set (data.volume_level 0–1) | volume_mute | turn_on | turn_off | toggle; other domains turn_on | turn_off | toggle via homeassistant.*; unsupported actions and out-of-range numbers → 400 validation_error; the state is re-read after the call
    • GET voice-targets?connection_id → { targets: [{ id, connection_id, entity_id, kind, name, tts_entity_id, is_enabled, last_announced_at, last_error, state }] }; GET voice-targets/discover?connection_id → { satellites: [{ entity_id, name, state, already_added }], media_players: [{ entity_id, name, state, already_added }], tts_engines: [{ entity_id, name }] }
    • POST voice-targets { connection_id?, entity_id, name?, tts_entity_id? } → { target } — kind derived from the domain (assist_satellite | media_player, anything else 400 unsupported_voice_entity); name defaults to the entity's friendly name; media players require tts_entity_id (400 tts_entity_required); 409 target_exists on duplicate; PATCH voice-targets/:id { name?, tts_entity_id?, is_enabled? } → { target }; DELETE voice-targets/:id → { ok: true }
    • POST voice-targets/:id/announce { message (1–1000 chars), preannounce? } → { ok: true, target } — assist satellites via assist_satellite.announce, media players via tts.speak with the target's tts_entity_id; 409 target_disabled when disabled; updates last_announced_at / last_error
    • POST announce { message, target_ids?, target_names?, connection_id?, preannounce? } → { ok, results: [{ target_id, name, entity_id, ok, error?, message? }] } — announces on the listed targets (target_names matched case-insensitively against name and entity_id) or on all enabled targets when neither is given; without connection_id the candidates span all connections, with it only that instance; explicitly selected disabled targets are reported as { ok: false, error: "target_disabled" }; 404 target_not_found when nothing matches
    • GET automations?connection_id&search&include_bridge → { connection_id, automations: [{ automation_id, entity_id, name, state, enabled, last_triggered, mode, running, managed_by_clapilot, created_by }], total, bridge? } — every automation.* entity (sorted by name); automation_id is the config id from the entity's id attribute (null for YAML automations without one); managed_by_clapilot/created_by come from home_assistant_managed_automations (automations created through Clapilot); with include_bridge=1 the bridge object below is added
    • GET automations/bridge?connection_id → { connection_id, bridge: { service: "rest_command.clapilot", available: true | false | null, clapilot_base_url, yaml_snippet } } — available from Home Assistant's GET /api/services (null when it cannot be read); clapilot_base_url is app_settings.public_base_url, else PUBLIC_BASE_URL / CLAPILOT_PUBLIC_BASE_URL / NEXT_PUBLIC_APP_URL / CLAPILOT_EXTERNAL_URL, else the request's forwarded host (never localhost); yaml_snippet is the rest_command: block for configuration.yaml that posts payload as JSON to /api/automation-webhooks/{{ token }}
    • GET automations/:ref?connection_id (ref = config id or automation.* entity id) → { connection_id, automation: { …summary, editable, config | null, config_yaml | null, read_only_reason: null | "not_ui_managed" } } — config from Home Assistant's GET /api/config/automation/config/<id> (keys ordered alias, description, triggers, conditions, actions, mode, then the rest; id omitted); editable: false for automations defined outside automations.yaml; 404 automation_not_found when neither a config nor an entity exists
    • POST automations { connection_id?, config? | config_yaml? } → 201 { connection_id, automation, warnings? } — exactly one of config (JSON object) or config_yaml (one automation; a one-item list copied from automations.yaml is accepted; parsed with the YAML core schema, so on/off and times stay strings; custom tags such as !secret are rejected); Clapilot checks alias (1–255 chars), non-empty triggers and actions, description (string, max 4000), mode (single | restart | queued | parallel) and 64 KB, renames legacy trigger/condition/action, drops id, generates a numeric config id, and posts to Home Assistant, which validates, writes automations.yaml, and reloads; records the automation in home_assistant_managed_automations; warnings: ["clapilot_bridge_missing"] when the config calls rest_command.clapilot but Home Assistant has no such service
    • PATCH automations/:ref { connection_id?, config? | config_yaml? } → { connection_id, automation, warnings? } — config_yaml replaces the whole automation; a JSON config replaces only the top-level keys it contains (null removes a key; a plural key also replaces its legacy singular form); 409 automation_not_editable for automations outside automations.yaml
    • DELETE automations/:ref?connection_id → { ok: true, connection_id, automation_id } — deletes the config in Home Assistant, the Clapilot record, and a dashboard tile of the automation entity; 409 automation_not_editable as above
    • POST automations/:ref/action { connection_id?, action: "enable" | "disable" | "trigger", skip_condition? } → { ok: true, connection_id, automation } — automation.turn_on / turn_off / trigger (trigger skips conditions unless skip_condition: false); works for read-only automations by entity id; 409 automation_not_loaded when Home Assistant has no entity for the automation
    • automation config endpoints need a Home Assistant administrator token: a 401/403 from the config API maps to 422 home_assistant_admin_required; Home Assistant's validation errors (HTTP 400) map to 400 automation_invalid with Home Assistant's message; YAML syntax errors are 400 validation_error with the parser message
    • errors { error, message? }: 409 no_connection, 404 connection_not_found, 400 invalid_base_url, 400 token_required, 422 home_assistant_auth_failed, 422 home_assistant_admin_required, 502 home_assistant_unreachable, 502 home_assistant_error, 400 invalid_entity_id, 400 invalid_service, 404 tile_not_found, 409 tile_exists, 404 target_not_found, 409 target_exists, 409 target_disabled, 400 tts_entity_required, 400 unsupported_voice_entity, 404 entity_not_found, 404 automation_not_found, 409 automation_not_editable, 409 automation_not_loaded, 400 automation_invalid, 403 forbidden, 401 unauthorized, 405 method_not_allowed, 404 not_found, 400 validation_error
    • details: Home Assistant
  • /api/modules/grocery/api/* (bundled Grocery / Einkauf module; list, orders, products, shops, and settings are workspace-global — no admin gates, created_by/added_by are attribution only; authenticated like all module handler APIs — session cookie or agent service auth; every endpoint returns {"error":"module_not_installed"} while the module is disabled; quantities and amounts are JSON numbers, dates YYYY-MM-DD, "today" is computed in Europe/Berlin)
    • GET health → { status: "ok", module: "grocery" } (no auth)
    • GET overview → runs planning, then { settings, plan, items, due_soon, recent_orders, shops, counts: { products, tracked, orders, list }, planned_added }; plan = { next_order_date, next_order_date_source (manual|cadence|default), cadence_days, cadence_source (override|history|default), last_order_date, auto_plan_enabled, due_count, planned_on_list }
    • GET list?refresh=0 → { items, plan, planned_added } (items ordered open → in_cart → unavailable, then position); list item = { id, product_id, order_id, name, quantity, unit, note, source (manual|planned|agent|reorder), status (open|in_cart|unavailable), position, added_by, created_at, updated_at, product: { auto_plan, interval_days, next_due_date, shop_refs, display } | null }; display = { image_url, url, shop_key } — what clients show: the product's explicit image_url / product_url, otherwise the image / link of the most recently stored shop article (shop_key = that shop, null for an explicit link)
    • POST list/items { items: [{ name, quantity?, unit?, note?, product_id? }] (1–50), source? (manual|agent|reorder), merge? (add|max) } or a single item object → 201 { items, created, merged } (one row per product; an existing row merges its quantity); PATCH list/items/:id { name?, quantity?, unit?, note?, status?, position? } → { item }; DELETE list/items/:id?snooze=0 → { deleted: true, snoozed_product_id } (planned items snooze their product until the next order date unless snooze=0); POST list/remove { ids (1–100), snooze? } → { deleted }
    • GET orders?limit&offset&status&shop_key&include_items=1 → { orders, total }; GET orders/:id → { order } with items; order = { id, shop_id, shop_name, shop_key, status (draft|cart_prepared|placed|delivered|cancelled), ordered_at, delivery_date, delivery_window, external_order_id, total_amount, currency, notes, source (manual|list|agent|import), created_by, created_at, updated_at, item_count, items? }; item = { id, product_id, name, quantity, unit, unit_price, total_price, status (ordered|unavailable|substituted), shop_product: { name?, url?, image_url?, id? }, position, display: { image_url, url, shop_key } } (display prefers the ordered article, then the linked product's display)
    • POST orders { shop_id? | shop_key?, status? (default placed), ordered_at?, delivery_date?, delivery_window?, external_order_id?, total_amount?, currency?, notes?, source?, from_list?, list_item_ids?, items? (≤ 200) } → 201 { order, planned_added } — explicit items win (items[].list_item_id links list items), otherwise items are built from from_list (all open/in-cart list items without an order) or list_item_ids; cart_prepared/draft attach linked list items (in_cart), placed/delivered delete fulfilled linked items and mark unavailable ones; shop_product is merged into the product's shop_refs[shop_key]; 409 duplicate_order (+ order_id) for a repeated (shop, external_order_id)
    • PATCH orders/:id { status?, ordered_at?, delivery_date?, delivery_window?, external_order_id?, total_amount?, currency?, notes?, items? (full replacement) } → { order, planned_added } — to placed/delivered removes attached list items, to cancelled returns them to open; DELETE orders/:id → { deleted: true } (attached list items return to open); POST orders/:id/reorder → { items, created, merged }
    • GET products?search&filter (all|tracked|due|frequent|archived)&limit&offset → { products, total }; product = { id, name, category, unit, default_quantity, auto_plan, interval_days_override, snoozed_until, shop_refs (per shop { name?, url?, image_url?, id?, unit_price?, updated_at }), image_url, product_url, display: { image_url, url, shop_key }, notes, archived, created_at, updated_at, stats: { order_count, last_ordered_date, interval_days, interval_source (override|history|null), typical_quantity, next_due_date, due_status (due|soon|ok|unknown), on_list } }; POST products { name, category?, unit?, default_quantity?, auto_plan?, interval_days_override?, notes?, image_url?, product_url? } → 201 { product }; PATCH products/:id { name?, category?, unit?, default_quantity?, auto_plan?, interval_days_override? (null clears), snoozed_until? (null clears), notes?, image_url? (http(s) URL, null clears), product_url? (http(s) URL, null clears), archived?, shop_ref?: { shop_key, name?, url?, image_url?, id?, remove? } } → { product, planned_added }; DELETE products/:id → { deleted: true }; POST products/import { products: [{ name?, category?, unit?, default_quantity?, auto_plan?, notes?, shop_ref?: { shop_key, name?, url?, image_url?, id?, unit_price? } }] (1–200) } → { products, created, updated } — upsert by name (name defaults to shop_ref.name; duplicates inside one batch collapse), existing products keep their edits (only empty category/unit are filled, auto_plan changes only when passed, archived products are restored), shop_ref is merged into shop_refs[shop_key]; no orders are recorded
    • GET shops → { shops } (installs the REWE shop-skill template once for enabled REWE shops); shop = { id, shop_key, name, kind (generic|rewe), base_url, is_enabled, is_default, browser_profile_id, browser_profile_name, fulfillment (delivery|pickup), settings: { postal_code?, market?, min_order_value?, notes? }, agent_skill, agent_skill_installed, order_count, last_order_at, created_at, updated_at }; POST shops { name, kind?, base_url?, shop_key? } → 201 { shop }; PATCH shops/:id { name?, base_url?, is_enabled?, is_default?, browser_profile_id? (one of the caller's own profiles, or null), fulfillment?, settings? (merged), agent_skill? } → { shop }; DELETE shops/:id → { deleted: true }
    • GET browser-profiles → { profiles: [{ id, name, provider, status, last_used_at }] } — the calling user's own persistent browser profiles
    • GET settings → { settings: { next_order_date, order_interval_days, auto_plan_enabled, updated_at }, plan }; PATCH settings { next_order_date? ('YYYY-MM-DD' | null), order_interval_days? (1–90 | null), auto_plan_enabled? } → { settings, plan, planned_added }; POST plan/refresh → { plan, planned_added }
    • errors { error, message? }: 400 validation_error, 404 not_found, 409 duplicate_product, 409 duplicate_order, 401 unauthorized, 405 method_not_allowed, 500 grocery_module_error
    • details: Einkauf (Grocery)
  • /api/modules/3d-studio/api/* (bundled 3D Studio module; models and imported meshes are workspace-global — created_by/updated_by are attribution only; session cookie or agent service auth; {"error":"module_not_installed"} while the module is disabled)
    • GET models?q&limit → { models }; model summary = { id, title, revision, metrics, node_count, has_thumbnail, thumbnail_url, created_by, created_by_name, updated_by, updated_by_name, created_at, updated_at }
    • POST models { title?, document?, metrics?, thumbnail_png? (base64 PNG ≤ 400 KB) } → 201 { model }; GET models/:id → { model } (summary + document); PATCH models/:id { title?, document?, metrics?, thumbnail_png?, base_revision? } → { model } or 409 { error, current_revision }; DELETE models/:id → { deleted, id, title }; POST models/:id/duplicate { title? } → 201 { model }; GET thumbnail/:id → PNG
    • POST assets { name?, format? (stl|obj|3mf), size?, positions_b64 (float32 LE xyz), indices_b64 (uint32 LE, 3 per triangle) } → 201 { asset } (≤ 12 MB, ≤ 300,000 triangles, indices validated); GET assets?ids=a,b → { assets } with the base64 buffers; GET assets/:id → { asset }
    • document = { version: 1, units: "mm", params: [{ name, value, min?, max?, step?, label? }], nodes: [...], settings?: { bed: [x, y, z] } } — node types and fields: 3D Studio
  • POST /api/modules/3d-studio/export { model_id | document + title, format: stl|3mf|obj|glb|svg, target: download|documents, folder_id? } → file download (content-disposition: attachment) or { ok, document, documentUrl, filename, bytes, metrics, warnings }; 422 empty_model/invalid_model, 413 export_too_large (Dokumente cap 8 MB)
    • { model_id, target: "slicer", slicer: "bambu_studio" | "orca_slicer" } → { ok, slicer, open_url, file_url, filename, expires_at, bytes, metrics, warnings }; open_url is bambustudioopen://<file_url> (Bambu Studio, macOS User-Agent), bambustudio://open?file=<file_url> (Bambu Studio, other platforms), or orcaslicer://open?file=<file_url> with the file URL percent-encoded; file_url is signed and valid for 15 minutes; 400 without a saved model_id
  • GET /api/modules/3d-studio/slicer-file/<token>/<name>.3mf — public (no session; the HMAC-signed token is the credential) → the saved model as 3MF, exported as the user who created the link; 403 slicer_link_invalid, 410 slicer_link_expired, 422 empty_model/invalid_model
  • POST /api/modules/3d-studio/import multipart file (STL/OBJ/3MF ≤ 50 MB) or { document_id } (shared or the caller's private Dokumente file) → { asset: { asset_id, name, triangles, size_mm } }; 415 unsupported_format, 422 invalid_mesh (not watertight or no triangles)
  • GET /api/modules/3d-studio/manifold-wasm → Manifold WASM kernel for the editor (ETag, must-revalidate)
  • GET /api/modules/3d-studio/font → glyph outlines (Noto Sans, JSON) for text in the browser editor (ETag, must-revalidate)
  • POST /api/modules/3d-studio/evaluate (editor backend for the native iOS/macOS apps) { model_id?, document?, template?, template_params?, edits?: [...], save?: { base_revision?, title? }, ghosts? } — starts from document, else the stored model_id, else template, else an empty model; applies edits in order (every model3d_update_model operation plus { op: "add_shape", preset, mode?, parent_id? }, { op: "paste", nodes, offset?, parent_id? }, the resize handles' { op: "resize", ids, factors: [fx, fy, fz], anchor: [x, y, z], frame?: { position, rotation } }, which scales the objects by factors (0.001–1000) along the frame axes around anchor in frame coordinates, writing real dimensions where the shape allows, and the rotate arrows' { op: "rotate", ids, axis: [x, y, z], angle, center: [x, y, z] }, which turns top-level objects by angle degrees around the world axis through center); evaluates with the Manifold kernel; with save updates model_id (or creates a model) including metrics and thumbnail → { ok, document, created_ids, model: { id, title, revision, updated_at } | null, evaluation: { metrics, warnings, errors, node_boxes, node_frames (top-level nodes: { min, max, size, position, rotation }, bounds in the node's own frame after scale/mirror plus its resolved placement), mesh: { positions_b64, indices_b64, runs: [{ start, count, node_id, color }] }, ghosts: [{ node_id, positions_b64, indices_b64 }] } } (float32 xyz positions and uint32 indices, little-endian); 422 { error, errors } when an edit fails (nothing saved), 409 { code: "conflict", current_revision }
  • GET /api/modules/3d-studio/templates → { templates: [{ id, title, summary, params }] } (clients localize names by id)
  • /api/benchmark/* (bundled Benchmark module backend; authenticated, gated on the benchmark module being installed)
    • GET /api/benchmark/overview — localized scenario list with availability (missing modules, failed preconditions, scenario version), model catalog (client.listModels()), and recent runs
    • POST /api/benchmark/preflight (admin) — dry-run validation { scenarioSlugs[], modelRefs[], judge?, judgeModelRef?, repetitions? }; returns preflight with ok, blockingErrors, unknownModels, executable/excluded cells, and the resolved judgeModelRef without creating a run
    • GET /api/benchmark/runs — recent runs (each with options, summary, snapshot, preflight, cleanup, legacy); POST /api/benchmark/runs (admin) — start a run { scenarioSlugs[], modelRefs[], label?, judge?, judgeModelRef?, repetitions?, autoCleanup?, retentionMinutes? }, 201 with the created run, 422 when preflight blocks (unknown model, no executable cell), 409 while another run is active
    • GET /api/benchmark/runs/:id — { run, results, artifacts, cleanupAudit }; DELETE (admin) removes a finished run
    • POST /api/benchmark/runs/:id/cancel (admin) — cancel a queued/running run; queued results become cancelled, the in-flight scenario finishes first
    • POST /api/benchmark/runs/:id/retry (admin) — re-queue failed cells and re-judge judge_failed cells; 409 while active or when nothing is retryable
    • GET /api/benchmark/runs/:id/artifacts — cleanup preview grouped by documents/files/images/videos/datasets with counts, cleanup state, and audit log
    • POST /api/benchmark/runs/:id/cleanup (admin) — { artifactIds?: string[] }; deletes the selection (default: every pending deletable artifact) through the module APIs, idempotent, 409 while a cell is active or a cleanup is running; returns { run, artifacts, result }
    • GET /api/benchmark/runs/:id/export — JSON download with snapshot, preflight, summary (coverage, technical success rate, quality), comparability warnings, results, artifacts, and cleanup audit
    • details: Benchmark

Benchmark summaries are recomputed from stored cells on reads. In judge-enabled runs, completed cells without a valid stored judge are returned as judge_failed with null scores. Incomplete runs have no rank or score ordering. This applies to lists, details, agent tools and exports; see Benchmark for historical repair migration 325_benchmark_missing_judge.sql.

Video Studio gallery thumbnails

  • GET /api/modules/video-studio/api/thumbnail?name=<video-filename>&v=<revision>
    • Returns a JPEG of the first frame from a gallery MP4, WebM, or MOV, using the existing module authorization.
    • name is a filename inside video-studio/videos; directory traversal and symlinks outside that directory are rejected.
    • v is an optional client cache key (gallery modifiedMs-sizeBytes). The server keys its local .thumbnails cache from the actual source file revision and extracts missing frames with ffmpeg, including legacy videos.
    • This read-only presentation endpoint does not change gallery records or agent tool contracts.

Video Studio AI

These first-party routes are authenticated and workspace-global: every signed-in user reads, edits, and deletes the same shared set of AI projects, scenes, characters, and voices. owner_user_id is written at creation as creator attribution only and is never used as a read/write filter. Project detail responses are snapshots containing project, ordered scenes, and linked characters; the project-list response intentionally contains project summaries only.

  • /api/video-studio/models
    • GET: list enabled video-generation providers without secrets and return the configured/default provider-model pair. When app_settings.media_model_catalog.video is non-empty, providers[].models contains exactly those catalog entries grouped by provider and default is the catalog entry marked isDefault; otherwise the route merges configured models, short-timeout best-effort live discovery, and each provider's default_model, and omits providers that still have no selectable models. The response also includes musicProviders/musicDefault and image: { entries, default }. Image entries come from media_model_catalog.image; when that catalog is empty, the image section contains only the current effective image-generation provider/model.
  • /api/video-studio/characters
    • GET: return { characters } for the whole workspace. Each character exposes portraitStatus (generating, ready, or failed), portraitError, voiceSampleUrl, voiceSampleUpdatedAt, and metadata-backed voiceId; the workspace-relative voice path is not exposed through this public JSON route. Legacy rows with a portrait resolve as ready, while rows without a portrait resolve as failed. A generating row older than ten minutes is reconciled to failed when listed.
    • POST: create { character } from JSON { name, description?, prompt|appearance_prompt?, generatePortrait?, voice_id? } or multipart fields plus optional image, voice, voice_id, and elevenlabs_voice_id/elevenlabs_voice_name (the character's assigned ElevenLabs voice). A voice upload must be MP3, WAV, M4A, OGG, or FLAC and no larger than 25 MB; it is validated before character creation. With an uploaded image and generatePortrait=false, the source asset is also assigned as portraitImageId, the response is immediately portraitStatus=ready, and metadata.portraitMode=original marks a byte-preserving portrait without any image-provider or appearance-derivation call. The updated web and Apple clients default to this mode. With generatePortrait=true (the API's legacy default when omitted), the row and uploads are persisted first and the response returns with portraitStatus=generating; portrait generation and appearance derivation continue in the background. Upload failures expose the localized image error instead of a generic request error.
  • /api/video-studio/characters/[id]
    • PATCH: update any of { name, description, appearance_prompt, voice_id, elevenlabs_voice_id, elevenlabs_voice_name } and return { character }. voice_id is the optional preset name used by providers such as xAI; elevenlabs_voice_id assigns the character's ElevenLabs voice (preselected for scene voice changes, resolved automatically by video_studio_change_scene_voice, and used as the per-scene target voice for automatic project dubbing when no project dub voice is set); an empty string clears either. For generated portraits, a changed appearance_prompt starts background portrait regeneration and returns immediately with portraitStatus=generating. Original portraits (metadata.portraitMode=original) stay unchanged when text is edited; use the explicit /portrait POST to generate a new portrait.
    • DELETE: delete the character and return { ok }.
  • /api/video-studio/characters/[id]/portrait
    • POST: start canonical portrait regeneration with optional { prompt } and return the character immediately with portraitStatus=generating.
  • /api/video-studio/characters/[id]/voice
    • GET: stream the character's voice sample with its stored audio content type and private one-hour browser caching.
    • POST: replace the voice sample with multipart field audio (MP3, WAV, M4A, OGG, or FLAC, max. 25 MB) and return { character }.
    • DELETE: remove the stored voice sample and return { character }.
  • /api/public/video-studio/characters/[id]/voice?token=...
    • GET: stream a token-authorized character voice sample for provider ingestion. Video Studio creates these links only for Kie Seedance submissions, stores token hashes in character metadata, and invalidates public-access metadata whenever the sample is replaced or removed.
  • /api/video-studio/ai/projects
    • GET: return { projects } ordered newest first. Summaries include sceneCount, project status, imageModel, and versions, but not scene snapshots or a clipReady count. A storyboard_generating project older than ten minutes with no scenes is reconciled to failed when listed. Each summary also carries previewImageUrl: the start-frame URL of the lowest-index scene that has one, otherwise null. The project in scene snapshot responses carries the same field, including terminal failure snapshots, so client updates preserve gallery previews.
    • POST: persist a project from { prompt, target_duration_seconds?, scene_count?, character_ids?, aspect_ratio?, provider_slug?, model?, image_provider_slug?, image_model?, title?, voice_conditioning?, dubbing?, dub_voice_id?, use_end_frames? }, transition it to storyboard_generating, and return its snapshot immediately with 201. metadata.dubbing is strictly opt-in (default false; sending dubbing=true without a configured ElevenLabs provider returns the dubbingRequiresElevenLabs error — dubbing is ElevenLabs voice conversion that replaces each clip's voice track, never text-to-speech), metadata.voiceConditioning defaults to true independently of dubbing, optional dub_voice_id is stored as the project dub voice (metadata.dubVoiceId), and the selected/effective image model is stored in metadata.imageModel; storyboard and missing start-frame generation continue as one server-side background chain. Character IDs may also be supplied as any workspace character's portrait_image_id or source_image_id and are resolved to the character ID; any remaining unknown IDs return 400 and no project is created. Create requests are idempotent within 15 minutes per user: the effective parameters are hashed into metadata.createFingerprint, and an identical request resumes the matching recent project instead of creating a duplicate – the response then carries deduplicated: { reason: "in_flight" | "retry_storyboard" | "existing", projectId } (in_flight: storyboard still generating, returned as-is; retry_storyboard: the project failed before any scene existed and its storyboard was restarted; existing: the failed project already has scenes and is returned for resumption). Projects in storyboard_ready, generating, concatenating, or ready are never reused. A storyboard that fails because the agent runtime was unreachable stores the classified transport error (cause code, target URL, attempts) as the project error and the API answers with the storyboardRuntimeUnreachable message instead of a bare fetch failed.
  • /api/video-studio/ai/projects/[id]
    • GET: return the project snapshot.
    • PATCH: while draft, storyboard_ready, ready, or failed, update { title?, prompt?, image_provider_slug?, image_model?, aspect_ratio?, voice_conditioning?, dubbing?, use_end_frames?, music?, music_prompt?, finishing?, error_message? } and return { project }. finishing is a partial object merged over metadata.finishing (mode basic|polished, transitions, titleCard, headlines, captions, nameChips, logoOutro, outroName, outroUrl, beatSync, musicBpm 70–180, loudnessLufs -24…-9, clipQa, clipReview, clipQaAutoRetry, exports[]); invalid values fall back to the current setting and the result applies to the next final version. error_message accepts only the empty string and clears the stored project error (the dismissable error banner); real error content is owned by the pipeline. The image provider/model pair is resolved and stored together. aspect_ratio accepts only 16:9 or 9:16 and is restricted to draft, storyboard_ready, or failed; ready/running projects expose it read-only. Voice settings are independent booleans stored in project metadata; dubbing and voice_conditioning compose (conditioning shapes the native clip voices, dubbing converts them afterwards), and enabling dubbing without a configured ElevenLabs provider returns the dubbingRequiresElevenLabs error. music toggles one generated background-music track for the whole stitched video (stored as metadata.musicEnabled); music_prompt sets its style prompt (empty string resets to the automatic prompt derived from the project prompt). The track is generated once per prompt during bundling via the enabled music-capable AI Media provider and mixed softly under the final audio; a failed or missing music generation never blocks the bundle — the video is finished without music.
    • DELETE: delete the project and return { ok }.
  • /api/video-studio/ai/projects/[id]/storyboard
    • POST: transition an existing project to storyboard_generating, return the refreshed snapshot immediately, and regenerate its storyboard plus missing start frames as one server-side background chain.
  • /api/video-studio/ai/projects/[id]/scenes
    • POST: while draft, storyboard_ready, ready, or failed, append an empty ai scene at max(scene_index)+1 with pending status, continuesPrevious=false, and the closest supported default duration for the project's stored model. Returns the complete snapshot. A structural edit changes ready back to storyboard_ready.
  • /api/video-studio/ai/projects/[id]/scenes/order
    • POST: accept { sceneIds }, require the array to contain every current project scene exactly once, and atomically assign contiguous indexes in that order. The immediate (project_id, scene_index) uniqueness constraint is protected by a negative temporary-index phase before final indexes are written. Returns the complete snapshot.
  • /api/video-studio/ai/projects/[id]/frames
    • POST: generate every missing AI-scene start frame and return the refreshed snapshot.
  • /api/video-studio/ai/projects/[id]/music
    • POST: enable background music (if off) and generate the project's one music track without bundling: a failed or prompt-stale state is discarded and resubmitted, { regenerate: true } also replaces a kept ready track, and a matching in-flight/ready track is reused so repeated clicks don't spend provider budget. Returns the refreshed snapshot; the status poll advances a generating track even on ready projects.
    • GET: stream the ready music track (audio/mpeg, Range supported); 404 while no ready track exists.
  • /api/video-studio/ai/projects/[id]/dub
    • POST: re-dub every dubbable clip_ready AI scene in the background without bundling. Requires a configured ElevenLabs provider (dubbingRequiresElevenLabs error otherwise) and enables dubbing when off (provider voice references stay untouched). Optional { voice_id } stores a project-level target voice used for all scenes (metadata.dubVoiceId); omitting it clears the override so each scene falls back to its speaking character's assigned ElevenLabs voice (scenes with no resolvable voice fail with dubVoiceRequired). Each dub converts the scene clip's existing voice audio via speech-to-speech and replaces the voice track. Existing dub tracks are discarded and re-queued; conversion runs sequentially in the background and the status poll picks up any remainder after a restart. Returns the refreshed snapshot.
  • /api/video-studio/ai/projects/[id]/generate
    • POST: start eligible per-scene video jobs after every HTML/block scene is already clip_ready; optional { retryFailedOnly, restart, bundle_only, redub, add_music, regenerate_frames, provider_slug, model } can restrict submission, restart the project, and override the stored video selection. When eligible AI scenes still lack a start frame (only failed scenes with retryFailedOnly), the project is claimed into storyboard_ready first (a generating project is rejected with the invalid-state error before any scene changes), the missing frames are regenerated in the background with the stored image selection, and the clip fan-out is chained behind them; the immediate response is the storyboard_ready snapshot and callers poll the project until it reaches generating, ready, or failed. bundle_only=true requires every scene to be clip_ready and only re-runs the final concat into a new version without resubmitting any provider job. redub=true behaves like bundle_only but additionally discards every existing dub track and re-synthesizes all character voice dubs first. add_music=true behaves like bundle_only but additionally enables background music, clears any previous music state, and generates a fresh track before bundling — this adds music to an already finished video without regenerating any clips. regenerate_frames=true clears every AI scene's start/end frames and clips, then chains frame generation, clip generation, and bundling in the background. A changed provider/model is persisted before fan-out and every AI-scene duration_seconds is re-snapped to that model's supported values. restart=true on a ready project detaches every prior AI clip, preserves start frames, clears prior dub metadata, and submits a complete new run. Each clip uses the stored project aspect_ratio; it is not supplied again in this request. With voice conditioning enabled, OpenAI-compatible providers receive up to three workspace samples as multipart reference audio, Kie Seedance receives up to three tokenized public input.reference_audio_urls when a public base URL exists, and xAI receives one voice_id only for an unambiguous single scene preset. Returns the refreshed snapshot.
  • /api/video-studio/ai/projects/[id]/status
    • GET: poll/reconcile provider jobs, advance scene states, claim pending dubbing work, and run ffmpeg concatenation when all scenes are ready. The same call applies the generation watchdog (CLAPILOT_VIDEO_STUDIO_GENERATION_STALE_MINUTES, default 45): unfinished clip jobs older than the window, AI scenes without a clip job while the project claim is older than the window, and concatenating claims older than the window become failed with localized clipStale/clipJobMissing/sceneStalled/concatStale messages, and the project then reports generationStale naming the window and the number of affected scenes. When dubbing is enabled, dialogue scenes remain clip_ready while metadata.dubStatus advances through pending|dubbing|done|failed; concat waits for done and uses metadata.dubPath. Dubbing claims older than 15 minutes fail. Each concat writes videos/<output_slug>-v<N>.mp4 plus its matching thumbnail and appends metadata.versions[] using SQL now(); finalVideoPath/thumbnailPath point to the latest version. A legacy unversioned final is adopted as v1 before the next concat writes v2. A ready AI clip first runs the automatic clip check while its scene stays clip_generating (see the qa-sheet route); the concat applies the project's finishing settings (transitions, beat sync, polished HyperFrames composition, ducked and loudness-normalized audio) and then starts the configured finishing.exports for the new version.
  • /api/video-studio/ai/projects/[id]/cancel
    • POST: cancel the active project state, reset generating scenes, and return the snapshot in storyboard_ready.
  • /api/video-studio/ai/projects/[id]/versions/[version]
    • DELETE: delete one non-latest tracked MP4/thumbnail pair, its export folder (video-studio/exports/<slug>/v<N>/), and its metadata entry. The latest version cannot be deleted through this route.
  • /api/video-studio/ai/projects/[id]/versions/[version]/exports
    • POST: accept { profiles: ["web" | "social_vertical" | "square" | "app_store_preview" | "posters"] }, mark each profile rendering in versions[].exports (profiles already rendering are skipped), render them from the version's master in the background, and return 202 { project }. Each entry is { profile, status: "rendering" | "ready" | "failed", files[], width, height, durationSeconds?, sizeBytes?, error?, createdAt }; an entry still rendering after an hour reads as failed (error: "interrupted"). Unknown profiles are ignored; none left returns 400.
    • GET: ?profile=<profile>&file=<index> streams one ready file (MP4 with Range support, or a poster JPEG); download=1 sends it as an attachment named <slug>-v<N>-<file>.
  • /api/video-studio/ai/scenes/[id]
    • PATCH: update { video_prompt?, script?, duration_seconds?, character_ids?, continues_previous?, voiceover_text?, voiceover_speech?, transition?, headline?, kind?, html_source? }; transition ({ type, duration } or a type string; cut, fade, fadeblack, flash, wipeleft, wiperight, slideleft, slideright, smoothleft, circleopen, zoomin, dissolve; 0.2–1.5 s) is the transition into the scene, stored as metadata.transition and always a cut for the first scene; headline (max 80 chars) is stored as metadata.headline for polished videos; voiceover_text stores the per-scene narration text as metadata.voiceoverText (trimmed, max 4000 chars, empty string clears it) used by the voice-over preview route below. continues_previous is stored in scene metadata, is forced to false for scene 1, and lets start-frame generation reference the immediately preceding ready AI-scene frame. kind=html with html_source.videoSlug validates and links an existing Video Studio gallery slug. Block-rendered sources use html_source.blocks[], renderedVideoSlug, and renderStatus through the dedicated render route below. Character IDs may also be supplied as any workspace character's portrait_image_id or source_image_id and are resolved to the character ID; any remaining unknown IDs return 400 and the scene is not updated. Returns { scene }.
    • DELETE: while the project is structurally editable, delete the scene, require at least one scene to remain, and renumber the rest contiguously in one transaction using temporary negative indexes. Returns the complete snapshot.
  • /api/video-studio/ai/scenes/[id]/render-blocks
    • POST: accept ordered { blocks: [{ slug, durationSeconds?, textOverrides?: [{ find, replace, scope? }], contentPrompt? }] }, validate every slug and override find value against the installed Video Studio block catalog, optionally fill declared slots through the configured storyboard-model candidates, persist a token-guarded clip_generating claim, and return 202 { scene }. scope is html, js, or both; omission defaults to both. Explicit overrides win over AI-filled values for the same slot. An unchanged ready selection returns its existing scene without a render or audio reset; the server verifies the MP4 still exists and is probeable. Narration-only scene PATCH requests preserve the HTML source and render state. The background chain invokes the Video Studio module's render handler in-process with project aspect, block durations, and flattened scoped overrides; the handler composes the normal manifest and renders HyperFrames into versioned videos/ai-scene-<scene-id-prefix>-<render-token>.mp4. Success probes the MP4 and writes renderedVideoSlug, renderStatus=ready, renderAspectRatio, renderRequestHash, duration_seconds, and metadata.htmlRenderDurationSeconds. A matching-token failure is localized; a block render older than 15 minutes without a result reconciles to failed.
  • /api/video-studio/ai/scenes/[id]/dub
    • POST: require clip_ready and a non-empty metadata.voiceoverText on the scene, atomically claim metadata.dubPreview.status=generating with mode=voiceover, and return 202 { scene } while the narration is synthesized in the background as one track (the saved metadata.voiceoverSpeech provider/model/voice when set, otherwise the workspace TTS default). Success stores a workspace-relative M4A audioPath with status=ready; failure stores status=failed and error. Project polling marks a claim stale after ten minutes.
    • GET: for an authenticated workspace user, stream the ready preview as audio/mp4 with private no-store caching. Byte ranges are intentionally unnecessary for these short preview files.
    • DELETE: delete the preview file and remove only metadata.dubPreview, then return { scene }.
  • /api/video-studio/ai/scenes/[id]/dub/apply
    • POST: require a ready preview and mix it OVER the current scene clip's audio — the original track is kept, ducked to half volume underneath the narration (clips without an audio track get the narration alone; legacy previews without mode=voiceover still fully replace the audio). Atomically replace the deterministic applied MP4, keep the scene clip_ready, and store both the compatibility dubStatus=done/dubPath fields and metadata.dubApplied. The applied file becomes the scene's active clip and concat source. A ready project returns to storyboard_ready so the next generation creates a new version. Returns { scene }.
  • /api/video-studio/ai/scenes/[id]/voice-change
    • POST: accept { voice_id, voice_name? } for a clip_ready scene, atomically claim metadata.voiceChange.status=generating, and return 202 { scene } while the background chain extracts the current clip audio (applied dub clip when present), converts it with the ElevenLabs speech-to-speech voice changer, remuxes the converted track over the clip, and stores a workspace-relative replacement clipPath with status=ready (failure stores status=failed and error; claims older than ten minutes reconcile to failed). Concatenation prefers a ready voice-change clip over the dub/raw clip, and a ready project returns to storyboard_ready.
    • DELETE: while not generating, delete the converted clip, remove metadata.voiceChange, and return { scene } with the original audio active.
  • /api/video-studio/ai/voices
    • GET: return { voices: [{ voiceId, name, category, previewUrl, isCustom }] } from the configured ElevenLabs account — own/custom voices (cloned, generated, professional) first, then provider default voices. Errors surface the upstream ElevenLabs message.
    • GET: stream the active applied MP4 to authenticated workspace users with normal video range support.
  • /api/video-studio/ai/block-content
    • POST: accept { block: { slug, durationSeconds?, textOverrides? }, content_prompt }, load the block's curated or HTML-extracted textSlots, and ask the storyboard LLM route for strict JSON whose keys exactly equal the slot keys. Missing, extra, empty, non-string, refusal-shaped, or malformed output fails closed; success returns the canonical block selection with { find, replace, scope } overrides for user review before rendering.
  • /api/video-studio/ai/scenes/[id]/frame
    • POST: generate or edit the scene start frame with { prompt, mode: "generate" | "edit", image_provider_slug?, image_model? }; returns { scene }. Overrides apply to this call only; omission uses project.imageModel. If the chosen provider cannot edit images, edit routing falls back to an edit-capable configured provider while generation continues to honor the exact project selection. The result is dimension-probed and, when its ratio misses the project's canonical frame ratio by more than five percent, scale-to-cover center-cropped without padding and imported as a new lineage-linked generated image before the scene is updated.
  • /api/video-studio/ai/scenes/[id]/qa-sheet
    • GET: stream the JPEG contact sheet (eight frames, 4×2) made by the automatic clip check of the scene's current clip; 404 when the scene has no checked clip. The verdict itself is scene.metadata.clipQa ({ status: "checking" | "passed" | "retrying" | "flagged", generatedVideoId, retries, startedAt, checkedAt?, durationSeconds?, issues: [{ code, at?, detail? }], contactSheetPath?, reviewModel? }).
  • /api/video-studio/ai/scenes/[id]/retry
    • POST: resubmit one failed AI scene and return { scene }. Optional { provider_slug, model } overrides the provider/model for this scene submission only; with no override the stored project selection is used, while a provider-only override uses that provider's default model. The project default is never changed. Duration support is resolved against the effective model and duration_seconds is persisted only when it must be snapped so the UI matches the submitted clip. The used selection is returned as clipProviderSlug/clipModel and stored in metadata.lastClipModel. Optional { force: true } also permits a clip_ready scene: the old generated_video_id is detached, a ready project transitions back to generating, and the existing reconcile/concat path rebuilds the final MP4 once all clips are ready.

Social media video generation

  • /api/social-media/video-generation
    • POST: submit a text-to-video job through the configured livestream media-generation providers with { prompt, title?, model?, provider_slug?, duration_seconds? }; returns 202 { assetId, status: "generating" }
  • /api/social-media/video-generation/[id]
    • GET: poll one generation job; returns { assetId, status: "generating" | "ready" | "failed", mediaPath, thumbnailPath, error }; ready assets are attached to posts via the module's media/import endpoint with kind: "livestream-asset"

reMarkable integration

  • /api/integrations/remarkable/status
    • GET: return the signed-in user's reMarkable connection status, last sync time, last error, and enabled service flags
  • /api/integrations/remarkable/connect
    • POST: exchange the one-time 8-character reMarkable device code for persisted device/user tokens
    • DELETE: disconnect the signed-in user's reMarkable connection
  • /api/integrations/remarkable/sync
    • POST: perform a manual pull sync into Notizen and Dokumente; notebook .rm pages are converted into Notizen scribble_data, while PDF-only documents stay in Dokumente as previewable files under remarkable/pdf/...

X integration

  • /api/integrations/x/oauth/start
    • POST: start the signed-in user's X OAuth flow; accepts optional scope_presets[] and now defaults to identity plus tweet read/write plus media-write plus bookmark-read scopes so user-scoped posting, media upload, and bookmarks reading can be enabled from the same connection flow
  • /api/integrations/x/oauth/complete
    • GET: OAuth callback endpoint used by X after consent; persists or refreshes the signed-in user's X tokens and account metadata
    • POST: manual callback-complete helper for pasted callback URLs/codes in the settings UI
  • /api/integrations/x/oauth/status
    • GET: return the signed-in user's X connection status, granted scopes, reconnect requirement, token expiry metadata, and last OAuth/API error
  • /api/integrations/x
    • DELETE: disconnect the signed-in user's X integration and revoke stored tokens where possible
  • /api/integrations/x/me
    • GET: return the connected X account profile from users/me
  • /api/integrations/x/posts
    • GET: list the connected X account's own posts with optional max_results, pagination_token, exclude_replies, exclude_retweets, since_id, and until_id
    • POST: create a new X post with { text?, reply_to_tweet_id?, quote_tweet_id?, media_ids?, tagged_user_ids? }; accepts text-only, media-only, or mixed text+media posts, but rejects quote-posts with attached media and tagged_user_ids without media_ids; text-only posts require tweet read/write and users scopes, while media posts also require media.write
  • /api/integrations/x/posts/[id]
    • GET: load one X post by id
    • DELETE: delete one X post by id
  • /api/integrations/x/media
    • POST: multipart upload endpoint for one or more media files from the connected X account; accepts repeated file parts (or files), optional repeated alt_text, optional media_category, and optional shared, uploads to X, waits for async media processing when needed, and returns the uploaded media_id values for later POST /api/integrations/x/posts calls
  • /api/integrations/x/mentions
    • GET: list mentions for the connected X account with the same timeline query options as /api/integrations/x/posts
  • /api/integrations/x/timeline
    • GET: list the connected X account's reverse-chronological home timeline with the same timeline query options as /api/integrations/x/posts
  • /api/integrations/x/bookmarks
    • GET: list the connected X account's bookmarked posts with optional max_results and pagination_token; requires the bookmark.read scope (requested by default for new connections via the bookmarks scope preset; older connections need a reconnect)
  • /api/integrations/x/users/by-username/[username]
    • GET: resolve one public X user profile by username

Atomic appointment reservations

Migration 350_atomic_appointment_reservations.sql serializes active appointment creation and status restoration at the database boundary. Public, internal and agent callers share global cross-type availability, including pending reservations and before/after buffers on both appointments. Conflicts return the existing slot-unavailable error and do not send booking notifications. Cancelled records reserve no time. Deploy the migration before the application. Existing overlapping records are preserved and require explicit operator reconciliation.

Tasks

  • /api/aufgaben
    • GET: list tasks for the signed-in user session, scoped to shared boards plus the user's private boards, with optional board_id, mandant_id, configured status key, prioritaet, faellig, zugewiesen, wiedervorlage, tag, umsatzrelevant, sort, limit, and view query params; sort accepts frist_desc, wiedervorlage, or deal_wert. alle_offen means every status whose category is not done. Task records resolve both assignee and creator profile metadata; creator fields are returned as erstellt_von_name, erstellt_von_email, and erstellt_von_avatar_url. The default view=full includes normalized attachment URLs. view=compact keeps list fields but omits attachment payloads and the source context snapshot, view=summary returns aggregate total/urgent/overdue and per-status counts, and view=assignee_counts returns the filtered count map used by the task sidebar.
    • POST: create a new task; optional status must be a currently configured key, otherwise the first open category status is used. Manual tasks persist the same traceability shape with a manual source label, context snapshot, optional CRM/follow-up/deal fields, and optional attachments[] entries using the shared chat attachment shape (type, name, size, mimeType, optional data/filePath/url). Optional idempotency_key (max 240 chars) is scoped to the authenticated user and persisted as agent_idempotency_key (agent:<user id>:<key>), so keys of different users never collide; repeating a create with the same key returns the existing task with HTTP 200 and already_exists: true, duplicate_of, duplicate_reason: "idempotency_key" instead of inserting again (a unique-index race resolves the same way). Optional dedupe_recent: true (sent by the live agent tool unless allow_duplicate is set) additionally returns a still-open task with the same normalized title (lowercase, whitespace collapsed, trimmed) on the same board created within the last 24 hours as duplicate_reason: "title_match". The regular UI form sends neither flag, so manual same-title tasks remain possible.
  • /api/aufgaben/statuses
    • GET: return the workspace-global ordered status definitions as { statuses: [{ key, label, category, sort_order, is_system }] }.
    • POST: create a status with { label, category }; the server generates a unique stable ASCII key and appends it to the order.
  • /api/aufgaben/statuses/[key]
    • PATCH: rename with { label }, change a non-system category with { category }, or set { sort_order }. The three system keys keep their locked categories. Moving the last remaining status in the waiting category to another category is rejected with HTTP 400, checked against the rows locked in the update transaction, for the same reason as the delete guard below.
    • DELETE: delete a non-system status with ?reassign_to=<key>; task and scheduled-task rows are reassigned transactionally first. The last remaining status in the waiting category is rejected with HTTP 400, because Symphony pauses a run without completion evidence in a waiting status. Both waiting-status guard messages are localized like the other errors of this route (ui_language in the PATCH body, ?ui_language= or the UI language cookie for DELETE).
  • /api/aufgaben/statuses/order
    • PUT: reorder every status with { keys: string[] }; the array must contain each configured key exactly once.
  • /api/aufgaben/boards
    • GET: list shared boards, the signed-in user's private Privat board, and preset board templates (akquise, marketing, finanzen) so users can quickly create common task board structures
    • POST: create a board with { name, visibility_scope? } or create a preset board with { template_key: "akquise" | "marketing" | "finanzen" }; omitted scope creates a shared board
  • /api/aufgaben/boards/[id]
    • PATCH: rename a board with { name }
    • DELETE: delete a board; requires { target_board_id } so contained tasks are moved to the target board instead of being orphaned
  • /api/aufgaben/assignees
  • /api/aufgaben/[id]
    • GET: load one task plus optional traceability metadata when the task originated from inbox automation; the payload includes source type, a deep link back to the original email, a short context snapshot, and normalized attachments[] with authenticated preview/download URLs for stored attachments. Attachment URLs prefer the configured public_base_url.
    • PATCH: update task fields, optional CRM/follow-up/deal fields, and replace attachments[] with the normalized shared attachment shape; status must be a currently configured task status key.
  • /api/aufgaben/[id]/attachments/[index]
    • GET: authenticated inline/download endpoint for one stored task attachment; resolves the metadata index from the task's attachments[] and streams persisted data: bytes or redirects a stored url
  • /api/aufgaben/[id]/comments
    • GET: list task comments with author metadata, mentions, and normalized attachments[] with authenticated preview/download URLs for stored comment attachments
    • POST: create a task comment with text, optional attachments[], or both; an attachment-only comment is valid
  • /api/aufgaben/[id]/comments/[commentId]/attachments/[index]
    • GET: authenticated inline/download endpoint for one stored task-comment attachment, scoped through the parent task's board permissions
  • /api/aufgaben/[id]/delegate
  • /api/scheduled-tasks
    • GET: list scheduled tasks from DB metadata synchronized against native runtime jobs, including protected native system automations and bundled automations such as the Agent Orchestrator Supervisor for admins. Reads overlay current agent_jobs enabled/next-run/last-run state so system rows remain accurate even when the compatibility metadata row is stale. Bundled/system rows include protection metadata, are shown in their own UI section, and cannot be deleted; returns publicBaseUrl so the UI can render absolute webhook URLs
    • POST: create an automation with either trigger_kind="schedule" plus schedule fields, or one of the event triggers new_mail, new_document, new_calendar_entry, webhook; accepts optional execution_scope="user"|"team" (team is admin-only and runs as the built-in global_team_service principal while created_by remains the audit creator; team runs clear creator-bound user/chat/UI context and do not replay conversation history between runs), optional action="agent_prompt"|"performance_check" (default agent_prompt) — when action="performance_check" the automation is a deterministic instance performance/health probe (maps to the clapilotPerformanceCheck job payload; prompt becomes optional) and an optional performance_config object (windowHours, maxFailureRatePct, maxInteractiveP90Ms, maxProviderTimeoutsPerModel = maximum full-budget provider timeouts per provider/model in the window, default 24; an open provider circuit also fails the check) sets the lookback window and pass/fail thresholds; accepts optional profileImageUrl, optional model to pin the runtime model, optional mailbox_scope="all"|"personal"|"agent" for new_mail, optional webhook_token for pre-generated webhook URLs, optional notify_target to assign the automation's implicit destination (main_session, main_session plus sessionId for a concrete web chat, team_chat for Teamchat #general, team_chat plus roomId for a selected Teamchat channel/group, or approved channel_approval Telegram/Slack/WhatsApp/Signal/iMessage groups or Telegram/Slack/WhatsApp/Signal/iMessage DMs), and optional notify_result_mode="always"|"informational"|"errors"|"never" to control whether run results are posted there. Also accepts optional workflow_config for the node editor (schema_version:1, trigger/agent/output nodes, edges, node positions). The compatibility fields still define the primary trigger/agent/output; additional event/webhook trigger nodes can dispatch the same automation, and additional output nodes fan out result delivery to multiple validated targets. Agent nodes accept config.specialized_agent_id, config.model, config.prompt (per-step work order for chained agents), and config.skill_keys (max 12 installed-skill keys injected additively into that agent's run, for default and specialized agents). Multiple agent nodes are executed as a linear chain along agent -> agent edges; normalization enforces linearity (one incoming/outgoing agent edge per node, no cycles) by dropping violating edges. The first chain agent uses the flat prompt; each later agent receives the previous agent's output plus its own config.prompt. For saved workflows, output nodes are authoritative for result delivery; output nodes wired from an intermediate agent deliver that step's result, unwired outputs deliver the final chain result, and removing all output nodes makes the automation run-log-only. always posts all results including background status, informational posts failures and only user-relevant contextual success updates, errors posts failures only, and never posts nothing. Automation priority is no longer configurable and new rows default internally to mittel. Time-based automations still create native runtime jobs and accept 1-10080 minute intervals, event-triggered automations stay app-managed rows and normally dispatch direct native runs when events arrive; new_mail fires after mail AI analysis and respects the configured mailbox scope, the protected document post-extraction new_document automation persists revisions into scheduled_task_event_queue for sequential native draining and provider-limit resumption, other new_document automations receive enriched context after document AI processing, and each webhook delivery runs in a fresh event-scoped session when /api/automation-webhooks/{token} is called. Webhook runs never replay stored history or a prior session summary, so an already oversized legacy automation session is bypassed automatically; event session keys are deterministic for retries and capped at 500 characters.
  • /api/scheduled-tasks/[id]
    • GET: load one scheduled task plus recent runtime runs/events for its automation session
    • PATCH: toggle enabled state, manually trigger immediate execution via run_now=true (without mutating schedule timing), or update title, prompt, admin-only execution_scope, optional profile avatar URL, optional pinned model, optional mailbox_scope for new_mail, optional webhook_token for webhook, optional workflow_config node graph, notify_target, notify_result_mode, metadata, trigger kind, and supported schedules; switching between schedule and event modes converts the automation between native-job-backed and app-managed execution as needed. Automation priority is no longer part of the editable API surface
    • DELETE: remove the native runtime job when present and delete the DB row; preinstalled system and bundled automations are protected and can only be paused, not deleted
  • /api/scheduled-tasks/profile-icon
  • /api/agent-runtime/action-approvals (cookie session only; agent service tokens are rejected)
    • GET ?status=pending|approved|executing|executed|failed|rejected|expired: the signed-in user's approvals, newest first (max 50) → { approvals: ActionApproval[] }. ActionApproval = { id, status, toolName, effect, details: {key,value}[], chatSessionId, sourceType, attended, createdAt, expiresAt, decidedAt, executedAt, resultPreview, errorMessage }. resultPreview and errorMessage carry the tool's readable message (the raw tool output JSON stays server-side for the agent notice).
    • GET /{id} → { approval } (404 when missing or owned by another user).
    • POST /{id}/decision with { "decision": "approve" | "reject" } → { approval }; approving executes the stored call before responding, so the status is executed or failed. 409 { error: "approval_not_pending", message, approval } when already decided or expired.
    • GET /gated-tools → { enabled, tools: [{ name, effect }] }, the tools that currently need approval (used by the automation editor's pre-approval picker).
  • POST /api/internal/action-approvals/channel-reply (internal agent secret only; agent shell tools hold a run-scoped token and are rejected): the channel runtime forwards a direct message from a linked user { userId, sessionKey, text, shownApprovalIds? }, where shownApprovalIds are the approval ids of the last list sent in that chat, in list order. When text is only an approval reply (Freigeben, Ablehnen 2, Alle freigeben, Approve, Rifiuta, ...) it decides like the decision route: a number or alle resolves against shownApprovalIds; without a list, alle covers the approvals parked in that conversation and a bare reply decides a single one of them; anything else returns a numbered choice (same-conversation approvals first, else all of the user's pending ones). Returns { handled: true, replyText, decisions: [{ approvalId, status }], listedApprovalIds? } with replyText in the user's UI language; listedApprovalIds is set when a list was sent and is stored by the runtime for the next reply. Otherwise { handled: false } and the message goes to the agent as usual.
  • /api/scheduled-tasks and /api/scheduled-tasks/[id] accept and return pre_approved_tool_names: string[] (only approval-gated tool names are kept; an empty array clears the list).
    • POST: signed-in image-generation helper for the automation editor; accepts { titel?/title?, prompt?, trigger_kind?/triggerKind?, schedule_mode?/scheduleMode? }, generates a square automation avatar through the configured image-generation runtime using the Clapilot tint palette/style guide, stores it as a generated image asset, and returns { profile_image_url, asset } so the caller can save the URL on the automation
  • /api/automation-webhooks/[token]
    • GET/POST/PUT/PATCH: public token URL for one enabled webhook automation. Dispatches only the matching automation and forwards method, query params, selected request headers, and parsed JSON/form/text body as the event payload
  • /api/internal/automation-events
    • POST: internal-only endpoint guarded by x-clapilot-agent-secret or Authorization: Bearer <internal-secret>; dispatches event-triggered automations for new_mail, new_document, or new_calendar_entry. The protected document post-extraction automation durably acknowledges new_document after enqueue even if runtime-job reconciliation is temporarily unavailable; identical pending/processed payloads deduplicate, while changed payloads advance the queue revision.

Widgets (routes remain under /api/mini-apps)

  • /api/mini-apps
    • GET: list the current user's installed Widgets, optionally filtered by search and dashboard_only=true
    • POST: create a structured Widget with { name, description?, widget_definition?, latest_data? }
    • Widgets use widget_definition with supported types stats, list, table, notice, or sections
    • if widget_definition is omitted, Clapilot infers a structured widget from latest_data
  • /api/mini-apps/[id]
    • GET: load one Widget
    • PATCH: update { name?, description?, widget_definition? }
    • DELETE: delete the Widget
  • /api/mini-apps/[id]/data
    • PATCH: update the latest data payload with { data, source? }
    • when richer collection payloads arrive, basic inferred Widgets can auto-upgrade their structured layout during this data update
  • /api/mini-apps/[id]/dashboard
    • PATCH: update personal dashboard placement, per-widget dashboard settings, and visibility for the current user with { dashboard_visible?, dashboard_settings?, dashboard_x?, dashboard_y?, dashboard_w?, dashboard_h?, dashboard_z? }
  • /api/dashboard/layout
    • GET: load the current user's built-in dashboard widget layout map
    • PATCH: update one built-in widget placement for the current user with { widget_id, x?, y?, w?, h?, z? }

Email and drafts

  • /api/emails

    • GET: read-only list of unified personal mailbox messages from the persisted inbox cache when available, with a provider read fallback for an empty cache or explicit search. Accepts folder (INBOX, ARCHIVE, SENT, DRAFTS, TRASH, or a provider-specific folder key such as microsoft-folder:{id}), search, limit, offset, and optional search_scope (all default for searches, folder for folder-scoped search). Global searches walk available IMAP/iCloud folders, use all-mail Gmail search, and use Microsoft Graph message search so Outlook custom folders and subfolders can be returned. Inbox summary rows include mailbox_provider (imap, gmail, microsoft, or apple) / mailbox_address when a source mailbox is known and can also return optional sender_image_url plus sender_image_fit (cover or contain) when the sender maps to a matched Mandant profile or logo
    • the AI preview line and priority reason on inbox rows are written in the request UI language (ui_language=de|en|it or the clapilot_ui_language cookie); the same applies to /api/angela/emails
    • POST: explicit personal-inbox refresh used by the E-Mail refresh action. Accepts the same folder/search/paging fields in JSON, refreshes enabled providers, updates persisted summaries, and applies matching Gmail/IMAP inbox filter rules. Provider mutations therefore do not occur during normal GET navigation.
  • /api/emails/folders

    • GET: list personal mailbox folder options for the E-Mail UI. The response includes the unified system folders, custom IMAP folders from the configured Kanzlei mailbox, and nested Microsoft 365 / Outlook folders with provider-specific keys, depth, unread counts where available, and mailbox address metadata.
    • GET /api/emails/filter-rules: list the signed-in user's persistent sender/keyword rules as { rules: [{ id, pattern, enabled, target_folder, mark_read, move_to_folder, forward_to, forward_since }] }. POST { pattern, forward_to?, move_to_folder? } adds (or re-enables and overwrites the actions of) a case-insensitive pattern, PATCH { id, forward_to?, move_to_folder?, enabled? } updates one rule, and DELETE ?id= removes one. forward_to must be a single valid address that is not one of the user's own mailboxes; an empty string clears it. Errors return 400 with code invalid_pattern, invalid_forward_to, own_forward_address, or empty_rule (neither move nor forward). Matching Gmail and IMAP inbox messages are moved to Nicht relevante mails and marked read during inbox synchronization when move_to_folder is true; the destination label/folder is created on demand. Rules with forward_to forward new company-mailbox mail server-side at ingestion (see Runtime Flows).
    • POST /api/internal/emails/filter-forward (internal, x-clapilot-agent-secret only): called by the native IMAP ingestion with { userId, mailboxEmail, ruleId, uid, uidValidity, rfcMessageId, receivedAt, rawBase64 }. Returns { status: "sent", forwardTo }, { status: "duplicate" }, { status: "skipped", reason } (rule_inactive, missing_message_identity, loop_header, own_address, before_rule, too_large, no_smtp_account), or { status: "uncertain", error }.
  • /api/email-senders

    • GET: return selectable senders for manual compose, currently the configured local IMAP/SMTP identity plus enabled personal and Agent Google Gmail identities and Apple Mail
  • /api/emails/automation/backfill

    • POST: authenticated catch-up trigger for personal IMAP inbox rows that are still unanalysed; accepts { ids: string[] }, acknowledges queued work with 202 / { queued: true }, then loads cached or IMAP-backed message detail and runs the same prepared-answer workflow in the background batch. The inbox UI uses the per-message automation endpoint for Gmail, Microsoft 365 Mail, and Apple iCloud rows so provider-backed visible messages can also be analysed without opening the detail view first.
  • /api/emails/[id]

    • GET: loads one personal mailbox message plus prepared-answer metadata (automation) for the email detail flow, visible attachment metadata, linked private dokumente records for attachments, timing badges, detected tasks/deadlines/highlights, synced shared mandant context, friendly retry-safe failure states, detected sender language (detected_language: de, en, or it), and an optional prepared_document / document_action payload only when the workflow created a new private document draft, not merely because the mail contained an attachment; accepts folder. The automation payload can include assignment_suggestion with confidence, alternatives, and linked attachment document ids so low-confidence Mandanten/Vorgang matches stay reviewable. Inline/signature images such as image001.png are filtered out of the normal attachment list before import. Visible personal attachments are imported idempotently into the user's private Persönlich folder with source_type='email_attachment' and a stable source_id, then queued for document indexing; scanned PDFs fall back from pdftotext to local page OCR before optional vision extraction.
  • /api/emails/[id]/automation

    • GET: return the current prepared-answer automation state for one personal mailbox message as { automation } (status, summary, context_label, reply_reason, draft, task_action, calendar_action, highlights, processing); accepts folder and resolves provider-prefixed Gmail / Microsoft 365 / Apple iCloud message ids as well as cached IMAP messages. Clients poll this endpoint for progressive processing.stage updates (context → attachments → analysis → draft → done). automation.source_attachments[] entries can carry analysis_summary / extracted_text_preview once extraction finished, and automation.metrics includes attachments_considered (whether all analyzable attachments were content-extracted before drafting), attachments_pending_count, attachments_total_count, and attachments_skipped_count.
    • POST: ensure the prepared-answer workflow has run for the message (summary, Mandant context, reply decision, optional auto-generated reply draft with the user's outgoing signature appended) and return the same { automation } payload; reuses the persisted automation when it is already final. Consumed by the web inbox detail flow and the native Apple Mail detail view.
  • /api/emails/[id]/assignment

    • POST: confirm or change the Mandanten/Vorgang assignment for one prepared email workflow. Accepts { mailbox_scope, mailbox_email, mandant_id, document_ids? }, updates email_thread_automations.mandant_id, records a confirmed assignment_suggestion, and assigns the listed imported attachment documents to the same Mandant.
  • /api/emails/[id]/attachments/[index]

    • GET: authenticated on-demand download for one visible personal mailbox attachment; accepts folder, resolves provider-prefixed message IDs for Gmail, Microsoft 365 Mail, and Apple iCloud Mail, and streams the selected attachment with a download content disposition instead of storing attachment bytes in the mailbox cache. The index is based on the filtered visible attachment list, not raw inline MIME parts.
  • /api/emails/send

    • POST: send a manually composed message through the selected sender. Local identities use SMTP/IMAP sent-folder storage; Gmail identities use the connected Google Workspace Gmail send permission; Apple Mail identities use iCloud SMTP/IMAP with the saved app-specific password
  • /api/emails/[id]/delegate

    • POST: create or reuse a prepared reply draft for the selected personal mailbox message; accepts folder. The source message language is detected across supported UI languages and persisted as source_language; generated replies default to that same language.
  • /api/emails/[id]/actions

    • POST: apply email-detail quick actions such as create_task, create_calendar, mark_read, mark_unread, archive, delete, or move; accepts folder and optional { targetFolder }
    • Gmail personal mailbox rows support mark_read, mark_unread, archive, inbox restore, and trash through the Google Gmail gmail.modify scope; moving Gmail rows into provider-specific IMAP/Outlook custom folders remains unsupported because the personal inbox intentionally exposes Gmail as a label-backed mailbox, not as a foreign folder tree
    • Microsoft 365 / Outlook rows now support mark_read, mark_unread, archive, delete, and move into system folders plus provider-specific custom folder ids through Microsoft Graph Mail.ReadWrite; existing users connected before this scope upgrade must reconnect Microsoft 365 once to grant write access
    • successful mailbox mutations now also persist a UI mutation event so /emails can patch/flash the affected row in place, and create_task additionally emits an aufgaben refresh mutation for open task views
  • /api/emails/batch-actions

    • POST: apply mailbox mutations to multiple personal inbox rows in one request; accepts { action: "mark_read" | "mark_unread" | "delete" | "move", items: [{ id, folder }], targetFolder? } and returns per-message success/failure rows plus aggregate successCount / failureCount for optimistic inbox rollback handling. Uses the same provider paths as /api/emails/[id]/actions, including IMAP, Gmail, Microsoft 365, and Apple iCloud Mail Draft creation, updates, deletion, sending, and attachment additions/removals require a real user UUID, including for shared agent drafts. System subjects receive HTTP 400 with CLAPILOT_PERSONAL_USER_REQUIRED before database, checkpoint, file, or provider side effects. Shared read authorization is unchanged.
  • /api/drafts

    • GET: lists personal drafts by default (or scope=personal), requiring a user UUID; an authenticated system subject without one receives HTTP 400 with CLAPILOT_PERSONAL_USER_REQUIRED before querying personal drafts. scope=agent retains shared agent-draft listing for authenticated system subjects.
    • POST: create a personal email draft with { von, an, cc?, betreff, inhalt, inhalt_html?, mandant_id?, quell_email_id?, agent_mailbox_email?, source_language?, reply_language? }. For personal drafts, the current user's configured outgoing email signature is appended before persistence unless the body already ends with that signature. When an HTML signature is configured, Clapilot stores inhalt_html as the HTML alternative and keeps inhalt as the plaintext fallback; uploaded signature logos remain embedded data images in drafts and are converted to CID inline assets during send. von is kept only when it is a mailbox the draft can be sent from (for personal drafts: the user's SMTP accounts, Gmail, or Apple Mail connection, plus the legacy agent address; for agent drafts: the agent mailbox); any other address, for example one copied from an incoming mail's To header, falls back to the default mailbox of that scope. The Athlete-Brand Matching module uses this endpoint for outreach drafts, then deep-links to /emails?tab=entwuerfe&draft=:id for review and sending
  • /api/drafts/[id]

    • GET: returns an authorized draft. Authenticated system subjects may read configured agent-mailbox drafts; personal records return HTTP 403 before any personal account lookup.
    • PATCH: updates draft fields (an, cc, betreff, inhalt, inhalt_html). Passing { reply_language: "de" | "en" | "it" } on an auto-generated source-email draft regenerates the reply from the cached original message in the requested supported language, preserving the user's outgoing signature for personal drafts.
    • POST /api/drafts/:id/send: sends a reviewed draft idempotently. SMTP success and the IMAP Sent-folder copy are persisted separately with a stable Message-ID. The response includes delivery.smtp, delivery.sentCopy, delivery.sentFolder, and delivery.messageId; an append failure returns ok: true plus a warning, and repeating the request retries only the copy. IMAP Sent discovery uses \\Sent SPECIAL-USE and common IONOS/Gmail/Outlook aliases; IMAP_SENT_FOLDER and AGENT_EMAIL_SENT_FOLDER provide explicit mappings. Before delivery the stored von is re-checked the same way as on create and rewritten to the scope's default mailbox when it cannot send. When nothing can send the draft, the localized 400 names the cause: no mailbox connected for the sender (api.drafts.error.senderMailboxMissing), agent mailbox not configured (api.email.error.agentMailboxNotConfigured), or no SMTP server for the sending account (api.drafts.error.smtpMissing).
  • /api/drafts/[id]/send

    • POST: sends a draft and now returns only user-facing failures for the prepared-answer UX
    • Draft attachments: every draft row carries attachments: [{ document_id, name, mime_type, size }] (JSONB, default []). Send attaches the referenced Dokumente files as multipart/mixed parts for SMTP/IMAP, Gmail, and Apple iCloud Mail. A missing file fails the send with a localized 400 before any delivery is claimed. Limits: 20 attachments and 25 MB total (413).
  • /api/drafts/[id]/attachments

    • POST: attach files to an open draft. multipart/form-data with one or more file fields stores each upload as a Dokumente record under _inbox/email-attachments. Personal-mailbox uploads are private to the uploader in the personal folder; agent-mailbox uploads are shared. Alternatively, JSON { "document_ids": ["..."] } attaches existing documents the user can read (shared, or private and owned by the user). Returns the updated draft row. Errors: 400 (no file, draft already sent, too many), 403, 404 (draft or document), 413 (over 25 MB).
  • /api/drafts/[id]/attachments/[documentId]

    • DELETE: detach one document from an open draft (the Dokumente record is kept). Returns the updated draft row.
  • POST /api/drafts and PATCH /api/drafts/[id] also accept attachment_document_ids (replace the list). PATCH additionally accepts add_attachment_document_ids and remove_attachment_document_ids.

  • /api/email-recipient-suggestions

    • GET: signed-in compose autocomplete with q and limit (1-200, default 100); merges Mandanten, app users, and previously used draft recipients into deduplicated suggestions
  • /api/emails/[id]/inline/[contentId]

    • GET: authenticated inline image (CID) delivery for one personal mailbox message so HTML bodies can render embedded images; the agent mailbox mirror is /api/angela/emails/[id]/inline/[contentId]
  • /api/internal/emails/automation/personal

    • POST: internal-only endpoint guarded by x-clapilot-agent-secret; runs the personal-inbox auto-analysis batch (runPersonalEmailAutomationBatch) and returns { started, skipped, failed }; no-ops with disabled: true when email_auto_analysis_enabled is off
  • /api/email-settings

    • GET: load the current user's outgoing email signature fields (outgoing_email_signature, outgoing_email_signature_html) plus a legacy compatibility view of the primary mail account (kanzlei_email, has_password, now sourced from user_email_accounts). If no saved signature exists, Clapilot can refresh outgoing_email_signature_suggestion from recent sent personal drafts and connected IMAP/Gmail/Microsoft/Apple Mail sent mail samples.
    • POST: update plaintext outgoing_email_signature and sanitized outgoing_email_signature_html. Sanitized HTML signatures may include normal links (https, mailto, tel) and uploaded PNG/JPG/GIF/WebP logos as data images up to the send-time inline asset limit; unsupported data images are removed. For onboarding compatibility, kanzlei_email/kanzlei_email_password in the body upsert the user's company mail account in user_email_accounts; mailbox management otherwise lives in /api/integrations/email-accounts.
  • /api/integrations/email-accounts

    • GET: lists the signed-in user's IMAP/POP3 mail accounts (accounts[] with account_type company/custom, address, display name, custom server details, has_password) plus company_mail_server_configured (whether the admin default IMAP server is set).
    • POST: creates (no id) or updates (id) an account. company accounts only take email_address/email_password and require the admin default mail server; custom accounts additionally take imap_host/imap_port (required host) and optional smtp_host/smtp_port. After saving, the IMAP login is verified (skippable with skip_verify: true); on failure the account stays saved and the response carries verified: false plus verify_error. Multiple accounts per user are supported; addresses are unique per user.
    • DELETE: removes an account by id query parameter.
    • Mailbox passwords are write-only and encrypted at rest (AES-256-GCM enc:v1: via src/lib/secret-crypto.ts): user_email_accounts.email_password, the agent mailbox app_settings.agent_email_password, app_settings.epost_password, and the legacy user_profiles.kanzlei_email_password. Responses only expose has_password / has_agent_email_password / has_epost_password. Readers outside the Next.js app (scripts/*.mjs, the agent service's mailbox polling) decrypt with key-compatible mirrors (scripts/lib/stored-secret.mjs, services/clapilot-agent/src/stored-secret.mjs). Rows stored before encryption keep working and are encrypted by scripts/db-migrate.mjs on the next start (skipped when neither CLAPILOT_AGENT_CONFIG_SECRET nor AUTH_SECRET is set).
  • /api/integrations/email-accounts/status

    • GET: { connected, account_count, company_mail_server_configured } for the App Verbindungen page and the Apple client status card.

Call & Fax Agent

  • /api/call-agent/config
    • GET: admin-only load of the singleton SIP/router configuration record plus provider-specific Realtime model dropdown options derived from configured ClapilotAICore provider rows
    • POST: admin-only upsert of the singleton SIP/router configuration record, returning the saved config and refreshed Realtime model dropdown options
    • config now includes published_ip and outbound_published_ip so inbound and outbound NAT/public-address routing can be managed in the UI instead of only via env vars
    • config now also persists realtime_provider, realtime_model, and realtime_voice so the phone worker can target either OpenAI Realtime or Google Gemini Live
    • config also persists fax capability on the same SIP line: fax_enabled, fax_station_id, fax_header_text, fax_transport_mode, fax_supports_inbound, and fax_supports_outbound
    • config now also persists approved_incoming_numbers; only those inbound caller numbers get the full internal Call Agent tool surface, while all other inbound calls are limited to general public information about clapilot.com
  • /api/call-agent/status
    • GET: signed-in runtime status snapshot combining the persisted worker state with the active SIP identity and feature toggles
    • when inbound calling is enabled, the native worker now keeps a live SIP listener registered and exposes its current registration state here
    • the status payload includes realtime_provider in addition to the active realtime_model and realtime_voice
    • shared-line runtime status now also includes active_kind, active_fax_id, and fax feature toggles so the UI and agents can respect the voice-or-fax lock
    • the status payload now also exposes sip_local_port, published_ip, outbound_published_ip, and expected_inbound_udp_ports so inbound forwarding/listener mismatches can be diagnosed from the product UI
    • if CLAPILOT_CALL_AGENT_SIP_LOCAL_PORT is set in env, the status payload exposes that effective runtime port even if the stored config record still contains a different sip_local_port
  • /api/call-agent/calls
    • GET: signed-in recent call history from call_agent_calls
    • each call row may include a generated post-call summary in metadata_json.summary
    • live-call summaries are generated from captured caller transcripts, assistant transcripts, and in-call tool results when available
  • /api/call-agent/faxes
    • GET: signed-in recent fax history from call_agent_faxes, including linked document and mandant labels when available
    • outbound rows end in sent only after the native g711 fax bridge reports a successful transmission; failed real sends stay failed with audit metadata
  • /api/call-agent/faxes/[id]
    • GET: signed-in fax detail with fax audit events from call_agent_fax_events
  • /api/call-agent/faxes/send
    • POST: signed-in outbound fax enqueue request for either an existing document_id or plain text_content; the native worker reuses the Call & Fax Agent SIP line, rejects the request while the shared line is already active with voice or fax, and in fax_transport_mode=g711 renders the transmission into TIFF before starting a real outbound SIP fax call
  • /api/call-agent/faxes/[id]/retry
    • POST: signed-in retry endpoint for failed, blocked, or cancelled faxes
  • /api/call-agent/faxes/[id]/cancel
    • POST: signed-in cancel endpoint for queued or active outbound faxes
  • /api/call-agent/customers
    • GET: signed-in customer lookup for the Call Agent module; searches Mandanten by name, company, phone, or email so the call UI can prefill the target number
  • /api/contacts/import/vcf
    • POST: signed-in multipart VCF import used by Settings -> Contacts; accepts file plus optional ui_language, parses standard vCard fields (FN, N, ORG, EMAIL, TEL, ADR), creates matching mandanten rows without triggering per-contact web enrichment, skips exact email duplicates already present in mandanten, and returns { parsed, created, skipped, failed, results[] }
  • /api/mandanten
    • GET: signed-in Mandanten list for the overview page; supports q, typ, activity, openTasks, deadlines (today, week, month), sort (first_name, last_name, organization, updated, created), and limit. Name sorting uses company/organization names for non-person clients, and the overview persists the selected order in the URL. The response returns the list metadata used by the Priorität / Aufgaben / Mails / Typ / Aktivität table view, including unread_email_count, unreplied_email_count, one direct mail target (email_link_mailbox_scope, email_link_id), and today/deadline context (today_deadline_count, today_deadline_title, due_soon_task_count, next_due_task_title, stale_unreplied_email_count, stale_unreplied_email_subject) so the UI can filter by Frist heute and deep-link into customer-specific work without a second API call
    • POST: signed-in Mandanten create path; after insert, Clapilot now attempts an optional web-profile match and persists website_url, logo_url, and profile_image_url only when the result clears the built-in confidence checks and the admin feature toggle mandant_profile_web_crawl_enabled is enabled. The shared enrichment path prefers Brave when available, but automatically falls back to headless browser search when Brave is missing or rate-limited
    • agent-driven mandanten_* tool writes now emit mandanten.client.updated UI mutation events so /mandanten list/detail views can refresh and highlight in place
  • /api/mandanten/[id]
    • GET: signed-in Mandanten detail fetch used by the manual create/edit form when an existing client is opened for editing
    • PATCH: signed-in Mandanten manual update path for the shared create/edit form; updates the core Stammdaten fields (typ, name, vorname, firmenname, adresse, telefon, email, steuernummer, rechtsform, branche) and returns the updated record
  • /api/mandanten/search
    • GET: signed-in lightweight Mandanten typeahead with q and limit (default 6); used by pickers and reference autocompletes
  • /api/mandanten/duplicates
    • POST: signed-in duplicate check before create/update with any of { typ, name, vorname, firmenname, email, exclude_id }; returns likely duplicate rows
  • /api/mandanten/[id]/ai-summary
    • GET: signed-in AI-generated customer summary for the Mandant detail page; cached, with force=1 to regenerate
  • /api/mandanten/[id]/emails
    • GET: signed-in list of up to 250 mailbox messages matched to the Mandant's known addresses for the customer communication tab
  • /api/mandanten/[id]/enrichment
    • POST: signed-in manual rerun for one Mandant’s web research; returns the refreshed Mandant row including enrichment_status, enrichment_source, enrichment_started_at, enrichment_last_checked_at, enrichment_error, and enrichment_suggestion; returns 409 when the admin feature toggle disables profile crawling. With body action: "accept_suggestion" it applies the pending review suggestion (filling only empty website_url/logo_url/profile_image_url fields, status matched); with action: "dismiss_suggestion" it clears the pending suggestion (status not_found); both action variants work independently of the crawl toggle
  • /api/mandanten/[id]/overview
    • GET: signed-in compact detail-page overview payload for /mandanten/[id]; returns actionable counters for offene/überfällige Aufgaben, Fristen heute bzw. diese Woche, unread/unreplied communication counts, workflow counters for linked calendar events, documents, notes, and email automation agent runs, the preferred direct mail target (email_link_mailbox_scope, email_link_id), and the latest matched mail so the default Vorgang tab can render a rule-based work-first summary without loading the full timeline
  • /api/mandanten/[id]/timeline
    • GET: signed-in merged customer timeline feed across received mails (email_thread_automations + mailbox cache), sent replies (email_drafts), created tasks, created calendar entries, and uploaded documents; accepts filter, limit, and offset for timeline pagination in the Mandant detail page
  • /api/aufgaben/live
    • GET: signed-in SSE bridge for Postgres NOTIFY updates on aufgaben; used by the task board to refetch changed tasks directly from DB state so status/column moves animate in place even when the initiating mutation did not originate from the visible chat stream
  • /api/admin/mandanten/enrichment
    • GET: admin-only status counts for Mandanten web research (pending, running, matched, not_found, skipped, error)
    • POST: admin-only backfill runner for existing Mandanten; accepts limit, optional statuses[], and optional force and processes the selected rows through the shared enrichment path with Brave plus headless-browser fallback; returns 409 when the admin feature toggle disables profile crawling
  • /api/call-agent/calls/start
    • POST: signed-in outbound call enqueue request; forwards to the native clapilot-agent call worker
    • active outbound calls now use the native RTP/live-audio bridge when CLAPILOT_CALL_AGENT_LIVE_AUDIO_ENABLED is not disabled
    • live caller transcription defaults to German (CLAPILOT_CALL_AGENT_TRANSCRIPTION_LANGUAGE=de) so phone-call transcripts stay in German unless intentionally overridden
    • the payload can now include a selected customer_id plus per-call allow_customer_info and allow_documents flags
    • live phone calls now forward Realtime function calls into /api/agent-runtime/tool-proxy using the initiating signed-in user as execution context
    • the live bridge now follows the Call Agent setting realtime_provider, using OpenAI Realtime for openai and Gemini Live for google_gemini
  • /api/call-agent/calls/[id]/end
    • POST: signed-in hang-up request for the selected active call; records ended_by from the current user
  • calls and faxes share the same line lock; voice start requests fail immediately while active_kind = fax
  • inbound calls are created directly by the native worker in call_agent_calls when the registered SIP listener receives a call
  • inbound calls now persist an access-policy marker in call_agent_calls.metadata_json; approved inbound callers receive full tool access through the built-in global_team_service principal, while unapproved callers stay in a public-info-only mode
  • inbound faxes currently enter call_agent_faxes only through the external/native receive handoff endpoint POST /internal/call-agent/faxes/receive, which writes the fax into mandanten/_inbox/..., links a placeholder dokumente row, and then hands it into the existing inbox/document processing flow
  • /api/call-agent/test-connection
    • POST: admin-only SIP registration probe through the native pjsua path
    • a successful probe unregisters before returning; it records probe metadata without changing the persistent worker's SIP registration or active-call state
  • native runtime endpoints under /internal/call-agent/* mirror the same fax surface for faxes, faxes/:id, faxes/send, faxes/:id/retry, faxes/:id/cancel, and faxes/receive

Agent mailbox

  • /api/angela/emails
    • GET: list agent mailbox messages; the dedicated Agent Google account's Gmail is preferred when enabled, with the configured agent IMAP mailbox retained as fallback. Accepts folder, search, limit, and optional force=1. Agent Gmail message IDs use the collision-safe gmail:agent: prefix. Inbox summary rows share the same optional sender_image_url and sender_image_fit enrichment as /api/emails
  • /api/angela/emails/[id]
    • GET: load one agent mailbox message plus automation metadata and linked dokumente records for visible attachments; accepts folder. Inline/signature images are filtered out before import. Visible attachments are imported idempotently into Dokumente with source_type='email_attachment' and a stable source_id
  • /api/angela/emails/[id]/attachments/[index]
    • GET: authenticated on-demand download for one visible shared agent mailbox attachment; accepts folder and resolves Agent Gmail or IMAP attachments
  • /api/angela/emails/[id]/delegate
    • POST: create or reuse a prepared reply draft for the selected agent mailbox message; accepts folder. The source language is stored with the draft and generated agent-mailbox replies default to that language.
  • /api/angela/emails/[id]/actions
    • POST: apply agent mailbox quick actions such as create_task, create_calendar, mark_read, mark_unread, archive, delete, or move; accepts folder and optional { targetFolder }. Agent Gmail mutations use the dedicated account's gmail.modify grant
  • /api/angela/emails/batch-actions
    • POST: apply agent-mailbox batch mutations with { action: "mark_read" | "mark_unread" | "delete" | "move", items: [{ id, folder }], targetFolder? }; responses include per-message results plus aggregate counts so web and Apple clients can optimistically update and refetch on partial failure
  • /api/angela/overview
    • GET: aggregate KI-Assistent dashboard data including running email jobs, recent autonomous activity, synchronized scheduled native jobs, and per-task log entries for the expandable dashboard cards

Hub catalog and distribution

  • /api/module-store/catalog
    • GET: signed-in module hub catalog for /modules; resolves the hub target from Admin Hub, serves the local hub registry directly in hub_mode=local, and otherwise proxies the configured remote hub
  • /api/module-store/publish
    • POST: admin-only publish of a workspace module to the configured hub target; writes into the local hub registry when hub_mode=local
  • /api/module-store/install
    • POST: admin-only install of one module version from the configured hub target into the local workspace
  • /api/skill-store/catalog
    • GET: signed-in skill hub catalog for /skills; resolves the hub target from Admin Hub, serves the local hub registry directly in hub_mode=local, and otherwise proxies the configured remote hub
  • /api/skill-store/publish
    • POST: admin-only publish of a workspace skill to the configured hub target; writes into the local hub registry when hub_mode=local
  • /api/skill-store/install
    • POST: admin-only install of one skill version from the configured hub target into the local workspace
  • /api/widget-store/local
    • GET: signed-in local widget list for /modules?tab=mini-apps, plus is_admin for publish/install controls
  • /api/widget-store/catalog
    • GET: signed-in widget hub catalog for /modules?tab=mini-apps; resolves the hub target from Admin Hub, serves the local hub registry directly in hub_mode=local, and otherwise proxies the configured remote hub
  • /api/widget-store/publish
    • POST: admin-only publish of one local structured widget to the configured hub target; writes into the local hub registry when hub_mode=local
  • /api/widget-store/install
    • POST: admin-only install of one widget version from the configured hub target into the local widget registry by slug
  • /api/agent-store/catalog
    • GET: signed-in specialized-agent hub catalog for /modules?tab=agents and Settings -> Agent -> Spezialisierte Agenten; resolves the hub target from Admin Hub, serves the local hub registry directly in hub_mode=local, and otherwise proxies the configured remote hub
  • /api/agent-store/publish
    • POST: admin-only publish of one local specialized agent to the configured hub target as a portable JSON definition; exported data excludes embed deployments, API keys, secrets, and runtime permission bypass
  • /api/agent-store/install
    • POST: admin-only install or update of one specialized-agent version from the configured hub target into the shared specialist catalog; install forces allowRuntimePermissionBypass=false
  • /api/v1/modules
    • GET: local hub catalog endpoint for published modules, available only when the instance runs in hub mode
  • /api/v1/modules/publish
    • POST: signed local hub publish endpoint for modules
  • /api/v1/modules/[slug]/[version]/download
    • GET: local hub artifact download endpoint for one published module version
  • /api/v1/skills
    • GET: local hub catalog endpoint for published skills, available only when the instance runs in hub mode
  • /api/v1/skills/publish
    • POST: signed local hub publish endpoint for skills
  • /api/v1/skills/[slug]/[version]/download
    • GET: local hub artifact download endpoint for one published skill version
  • /api/v1/widgets
    • GET: local hub catalog endpoint for published widgets, available only when the instance runs in hub mode
  • /api/v1/widgets/publish
    • POST: signed local hub publish endpoint for widgets
  • /api/v1/widgets/[slug]/[version]/download
    • GET: local hub artifact download endpoint for one published widget version
  • /api/v1/agents
    • GET: local hub catalog endpoint for published specialized agents, available only when the instance runs in hub mode
  • /api/v1/agents/publish
    • POST: signed local hub publish endpoint for specialized-agent JSON snapshots
  • /api/v1/agents/[slug]/[version]/download
    • GET: local hub artifact download endpoint for one published specialized-agent version

Chat and calendar

Public URL fetch boundary

Link previews, iCal imports and subscription synchronization resolve every URL and redirect to public IPv4/IPv6 destinations before connecting. The transport pins approved DNS answers, rejects mixed private/public answer sets and embedded credentials, and allows at most five redirects and two MiB decoded responses within each caller timeout. Public HTTP/HTTPS and webcal normalization remain supported; private-network feeds are rejected.

  • /api/chat/transcription/realtime/session

    • POST (authenticated): accepts { ui_language?: "de" | "en" | "it" } and requires the signed-in profile to have chat_preferences.speech_to_text_provider="openai_realtime"
    • also requires app_settings.api_live_transcribe_enabled=true; resolves the OpenAI API-key Realtime provider and compatible model selected in Settings -> ClapilotAICore -> Audio, then creates a transcription-only client secret with 24 kHz PCM input, automatic spoken-language detection, balanced-delay streaming, explicit turn commit, a two-minute connection TTL, and a hashed per-user safety identifier
    • returns { client_secret, expires_at, model, webrtc_url, websocket_url } with Cache-Control: no-store; it never returns the long-lived provider key. websocket_url intentionally has no model query because the transcription model is already bound inside the transcription session; passing it as a Realtime conversation model causes the upstream connection to be rejected.
    • web uses webrtc_url; Apple clients use websocket_url and send base64 PCM chunks. Provider/configuration failures are localized and surfaced instead of silently falling back to remote transcription
  • /api/chat/uploads

    • POST (authenticated): upload-first path for chat document attachments. Multipart with one file field; the file must be a Documents format (see /api/documents/inbox) of at most 100 MB. The file is stored as a shared dokumente row under _inbox/chat-uploads/... and queued for indexing. Returns { attachment: { type: "file", name, size, mimeType, documentId, filePath, url } }. Errors (localized { error }): 401 not signed in, 400 no file or empty file, 413 over 100 MB or unreadable body, 415 unsupported format, 500 storage failure
    • web (side chat and chat page) and Apple chat clients upload document files here right before sending and pass only { type: "file", name, size, mimeType, documentId } to POST /api/chat. Base64 inside the chat JSON grew a file by a third and hit the 100 MB Cloudflare request limit; images, audio, video and team direct messages still send inline data
    • inline video attachments (type: "file" with a video/* MIME type or an MP4/M4V/MOV/WebM/AVI name) are also written to _inbox/chat-uploads/... as shared dokumente rows during POST /api/chat, without a document reference card. The model still receives the clip inline as before; the stored personal and Team Chat rows keep documentId, and history responses expose url: /api/documents/:documentId/preview, which streams the file with HTTP Range support. Web chat, the floating chat, and the Apple chat clients render these as inline video players (web plays in place; macOS plays inside the card after a tap; iOS opens the full-screen player). A failed copy is logged and does not fail the turn; the clip is then only missing its replay
  • /api/chat

    • attachments[] entries of type: "file" may carry documentId instead of data. The server only accepts ids of chat uploads (_inbox/chat-uploads/...), reads the stored file and treats it like an inline document upload; an unknown id returns 400 with a localized "add the file again" error. Document files up to 10 MB (20 MB per message) are also handed to the model inline; larger ones stay referenced only, and the agent is told to read them via files_visualize (dokumente_id) or documents_get. Large inline uploads from older clients are handled the same way and are no longer stored as base64 in the chat row
    • base64 data URLs are parsed from the header only (src/lib/data-url.ts); running a regular expression over the payload overflowed V8's regexp backtrack stack from about 3 MB and returned an empty 500. Unexpected attachment failures now return a localized JSON error
    • normal chat keeps shell/bash snippets inside the normal agent prompt instead of intercepting them server-side
    • provider/model identity questions such as openai oder anthropic?, welches llm?, or bist du gpt-5.4? are answered deterministically from the current session runtime instead of relying on model self-reporting
    • the agent decides from full message context whether to execute a local command or just analyze/explain it
    • direct commands like whoami, pwd, git status --short, /run <command>, and fenced bash blocks are treated as normal user input, not as transport-level shortcuts
    • local shell execution and structured package installs now flow through the runtime agent tool path (exec_command, package_install)
    • Teamchat/group-chat turns can additionally bind the built-in global_team_service execution principal while still keeping persisted history on the room-scoped session key
    • attachment payloads still carry uploaded file bytes for supported image/document types, and may now also include optional hidden filePath metadata so web, floating chat, and Apple chat clients can reference the original file location without exposing that path in the visible transcript UI
    • personal history responses from GET /api/chat/sessions/:sessionId/messages replace persisted inline attachment data URLs with authenticated /api/chat/sessions/:sessionId/messages/:messageId/attachments/:attachmentIndex URLs. This keeps large historical images, files, and audio out of the history JSON while web, iOS, and macOS load the same user-owned bytes on demand. Stored chat videos (with documentId) instead get the Range-capable /api/documents/:documentId/preview URL, because the per-message route serves whole payloads only.
    • personal-chat document uploads remain active runtime context for up to four subsequent text-only user turns in the same session. The backend rehydrates the most recent stored file bytes for the agent run without copying them into each follow-up row; a new upload, explicit document/reply reference, or conversation-reset phrase replaces or clears that inherited context
  • personal-chat attachment payloads may include type: "audio" with base64 data, mimeType, name, optional durationMs, and optional hidden filePath; /api/chat stores the audio as a user-owned chat asset, runs STT on the backend, injects the transcript into the normal agent turn, and marks the turn for an assistant audio reply. Web and native file pickers classify imported WAV, MP3, M4A, and OGG files as audio attachments (up to 25 MB); in the Apple chat composer this payload can also be produced by pressing and holding the mic button, while a short mic tap remains speech-to-text dictation.

  • team-chat/group-chat turns sent through /api/chat may now also include type: "audio"; the backend runs the same STT step for prompt assembly and persists the recorded user audio inline on the room message so the team-chat timeline can keep rendering it after reload

  • finalized Team Chat agent and automation replies resolve owned audio references from .clapilot/agent-media/<owner>/... into persisted type: "audio" attachment metadata before local paths are removed. Workspace-local MP4, MOV, and WebM references are copied into that owner-scoped media store, registered with immutable provenance, and persisted as type: "file" metadata containing the attachment asset ID; delivery is not successful when registration fails (team_chat_post_message and /api/agent-runtime/assistant-message reject the message before persisting it, and successful responses return attachment_count/attachmentCount plus attachments[] with attachment_id/attachmentId and the authenticated url). Automation delivery carries the persisted service-principal ID as the media owner, matching first-party TTS/image creation instead of borrowing a creator or viewer user scope. First-party TTS creation records owner, canonical path, MIME type, size, and SHA-256 provenance in agent_media_assets; derived audio assembled by ffmpeg/HyperFrames must call media_register_audio once after writing the final file, which records or idempotently verifies the same provenance without synthesizing or uploading anything. Delivery requires that immutable record in addition to owner-directory containment and rejects traversal, symlink, hard-link, replacement, and type/size mismatches. The resolver accepts absolute, workspace-relative, and Markdown-link references and caps the combined audio payload at 15 MB across at most four files and each video attachment at 20 MB. Owned ![... ](/api/generated-images/<id>) automation output is materialized into a persistent Team Chat image attachment at the same delivery boundary; authenticated attachment URLs replace local paths so room viewers never depend on the automation creator's user scope. History responses expose stable authenticated attachment URLs instead of Base64 media; web and native Apple Team Chat timelines load those URLs on demand.

    • personal audio-origin turns now persist message_meta.audioReplyRequested = true, while the final assistant text reply can additionally persist message_meta.assistantAudio with the generated TTS attachment metadata (type, name, size, mimeType, url, optional transcript, optional durationMs)
    • assistant TTS audio replies are currently still limited to personal /chat; team-chat/group-chat audio turns currently return the normal text assistant reply only
    • personal chat and assistant-backed team-chat turns sent through /api/chat no longer support a deterministic /image <prompt> transport shortcut; image-related text remains normal chat input, and the runtime agent may dynamically call images_generate or images_edit when the full conversation context warrants it
    • when a current /api/chat turn includes uploaded image attachments, the backend imports those images as user-owned generated-image assets and passes their ids through clientContext.currentImageAttachmentAssetIds, allowing a later dynamic images_edit tool call to target the latest chat image without bypassing the agent decision
    • personal and team-chat requests may include documentReferences[] (document id/title/type plus optional metadata) from the # reference autocomplete flow; /api/chat persists those refs in chat_nachrichten.message_meta.documentReferences for personal chat and in chat_group_messages.message_meta.documentReferences for group-room Angela turns, then injects document summaries/excerpts into the current prompt as prioritized document context
    • personal-chat and team-chat Angela turns may also include replyReference (messageId, messageRole, authorName, text); /api/chat persists that quoted-message snapshot in message metadata and injects it back into the current prompt. The server also loads images/files from the quoted row's stored attachment JSON and from markdown image links to generated-image assets or workspace files, attaches materialized media only to the agent run, and passes existing generated-image asset ids through to image/video tools without duplicating those assets
    • for Team Chat, an agent-role replyReference also addresses the original agent, including in mention_only mode. The server resolves the message within the requested room and reads its persisted assistantAgentId / assistantAgentHandle; client-supplied author names cannot select the recipient. Bare UUIDs, web UUID-a / UUID-q, and Apple reply-UUID / msg-UUID IDs resolve to the stored row for routing and quoted media. Disabled or uninvited agents are not triggered; explicit mentions are combined with the reply recipient without duplicates. Main-agent replies use the same rule while respecting main_agent_enabled.
    • pending team-chat Angela turns now also persist chat_group_messages.message_meta.assistantToolStatuses while tools are still running so every room viewer can see the same live progress state in the sidebar Angela card
    • persisted assistantThinking (personal chat_nachrichten message meta only) is the reasoning of one assistant turn as a plain string, joined across multiple reasoning blocks with a blank line and capped at 20,000 characters. Clients render it as the collapsed Thinking disclosure next to the tool-call log; it is absent when the turn produced no reasoning. Team chat_group_messages intentionally never stores it, because a room's message meta is readable by every member
    • persisted assistantToolStatuses entries (personal chat_nachrichten and team chat_group_messages message meta) carry { id, label, state, toolName? }; toolName is the raw runtime tool identifier (for example Bash, emails_list_messages) and lets clients pick the matching icon in the chat tool-call timeline log. The retained list keeps the most recent 24 tool calls per assistant turn; web chat renders the latest five by default and reveals the older retained calls on demand
    • while a model is streaming a thinking block before its visible answer, web and Apple chat surfaces keep the latest ten rendered lines visible by default and provide an inline disclosure for the full block; collapsing again returns to the latest-ten-line view
    • clapilot.tool SSE frames may include event.todoList: Array<{ id, label, status }> with status = pending|active|done; each frame replaces the current checklist, and todo-producing tools are omitted from the generic tool-status list
    • personal and team assistant rows persist the latest checklist as message_meta.assistantTodos, including pending writes and the final write without forcing unfinished items to done; chat history and floating-chat history hydrate the same field after reload
    • web and Apple (iOS/macOS) chat surfaces render the checklist the same way: as an inline Plan n/m list above the working indicator while the turn is streaming, and as a collapsed Plan n/m disclosure on the finished assistant message; both personal chat and Team Chat hydrate it from message_meta.assistantTodos after reload
    • clapilot.tool SSE frames may include event.artifacts: Array<{ id, kind, title, href, moduleSlug?, action }> — structured linked-artifact references derived server-side from entity-mutating tool results (kind = document|canvas|note|task|wiki|calendar|mandant|email-draft|word|excel|video-project|video|file, action = created|updated, href always an internal app path). Unlike todoList, artifact frames accumulate: clients merge by id with later entries replacing earlier ones, created staying sticky, and a cap of 8 per assistant turn
    • personal and team assistant rows persist the merged list as message_meta.assistantArtifacts; web chat, the floating chat widget, and the Apple clients render it as clickable linked-artifact cards (icon tile, title, kind label, open affordance) that deep-link into the owning module (for example /dokumente?preview=…, /modules/canvas?file=…, /aufgaben/<id>, /modules/video-studio?aiView=board&aiProject=…). Entities covered by an artifact card are excluded from the regex-based Verknüpfte Referenzen fallback card on the same message
    • one enabled specialized @agentHandle mention in a personal chat turn routes that turn into the mentioned specialist; personal chat accepts one specialist target per turn. In Team Chat, every explicitly mentioned invited specialist is queued in parallel as one exclusive target set, so Angela does not also produce a duplicate reply
    • team-chat room specialists are stored as room-scoped invitations with reply_mode=mention_only|all_messages; unmentioned team-chat turns fan out to every invited enabled specialist whose mode is all_messages, using the same detached specialist pending/finalization flow as explicit @agentHandle mentions
    • team-chat specialist mentions run with the current user request plus a compact visible room-context bundle of the previous relevant team-chat messages (currently capped at 10), including prior named specialist replies; Angela/main-agent turns keep the normal broader team-chat transcript replay
    • the floating personal chat now uses the same specialized @agentHandle composer flow as /chat, so specialists can also be invoked from the sidebar widget
    • specialized @agentHandle mentions are now detached background tasks: /api/chat persists the pending specialist bubble immediately, returns control to the parent chat route without blocking on the specialist run, finalizes that same bubble later when the specialist task completes, and now persists live assistantToolStatuses updates for those backend specialist runs as tools start/finish
    • direct specialist replies are persisted as separate assistant rows instead of only reusing the original personal user-turn antwort; personal specialist rows use message_origin = assistant_specialist|assistant_specialist_delegated, async main-agent follow-up rows use message_origin = assistant_async_callback, while team-chat rows reuse sender_display_name plus assistant identity in message_meta
    • specialist assistant rows may carry message_meta.assistantAgentId, assistantAgentHandle, assistantAgentName, optional delegatedByAgentId, optional delegatedByAgentName, and invocationType = mention|delegation so the frontend can render named assistant bubbles consistently after reload
    • when the main agent delegates via delegate_to_specialized_agent, the tool now returns immediately with a queued task payload; once the specialist finishes, the backend asynchronously triggers a second main-agent run and persists that later follow-up as a separate assistant message
    • when the main agent fans out via spawn_clapilot_subagents, the tool creates one clapilot_subagent_tasks row per generic worker inside a clapilot_subagent_batches row, persists visible pending worker bubbles using message_origin = assistant_subagent in personal chat, and triggers one final async main-agent callback using message_origin = assistant_subagent_callback after every worker is completed or failed
    • when no explicit personal sessionId is provided, /api/chat now resolves or creates the user’s main personal session instead of falling back to the latest active custom session
    • the first user turn in a fresh custom personal session now triggers automatic session naming from that first prompt; manual session renames stay authoritative and are not overwritten later
    • group-chat requests accept groupChat=true plus optional roomId so /team-chat can keep the shared team room separate from future room-based chat variants while still reusing the KI-Assistent streaming path
    • each streamed turn records its native runtime gateway key as message_meta.assistantRunSessionKey on the pending user-turn/room row; because the runtime stores the chat message id as the agent_runs idempotency key, history reads reconcile stuck placeholders against the backing run: a terminal run without a delivered answer clears the placeholder within ~2 minutes (message_meta.assistantRunExpired), and a run that completed while no web producer was alive (e.g. web container restart mid-turn) back-fills its output_text as the reply (message_meta.assistantRunRecovered) instead of losing the answer; rows without run linkage keep the coarse created_at/updated_at staleness backstop
    • persisted KI-Assistent replies in personal chat sessions now also trigger Apple push notifications through APNs when device registration is configured
  • /api/chat/steer

    • signed-in endpoint used by the web chat queued-message "send now" control
    • POST with { sessionId, groupChat?, message?, attachments? } resolves the same native runtime sessionKey as /api/chat, then forwards to POST /internal/runs/steer
    • succeeds for running OpenAI-Codex subscription bridge turns through Codex app-server turn/steer
    • succeeds for running Claude subscription bridge turns by writing a realtime user message into the active claude -p --input-format stream-json --output-format stream-json process
    • also supports live steering for active native/embedded-PI runs: during a tool call the follow-up is injected into that tool result; during model generation the runtime aborts the active provider request, keeps the original stream alive, and immediately starts a continuation provider step with the follow-up
    • native steering is acknowledged only while a provider/tool step owns a guaranteed delivery path; the small transition/finalization gap returns 409 with reason=native_boundary_transition, leaves the browser message queued, and lets the normal queue dispatcher send it as the next turn
    • when no live steering channel exists, the native runtime checks the latest persisted agent_runs row instead of claiming that no native engine exists: still-running rows return 409 with reason=run_not_steerable and remain queued, while terminal rows that still own the pending chat placeholder return retryAsNewTurn=true with reason=run_interrupted|run_finished; an older completed run cannot trigger recovery for a newer turn still in preflight
    • for retryAsNewTurn=true, the web endpoint immediately reconciles the terminal run's stale assistant placeholder; the chat page and floating widget abort any stale browser stream, reload history, and let the existing queue dispatcher start the message as a new turn
    • assistant-origin automation/system alerts are persisted into the same personal chat timeline and reuse the same APNs delivery path
    • persisted KI-Assistent replies in group-chat rooms now trigger Apple push notifications through APNs when device registration is configured
  • /api/chat/stop

    • signed-in endpoint used by the chat composer stop control (web chat page and floating widget)
    • POST with { sessionId, groupChat? } resolves the same native runtime sessionKey as /api/chat/steer, finalizes every pending assistant placeholder in that personal session or team room (assistant_pending = FALSE, stop notice, message_meta.assistantRunStopped = true), and forwards to POST /internal/runs/abort for the session's default gateway key plus every distinct message_meta.assistantRunSessionKey recorded on the cleared placeholders
    • works without a live browser stream: after a reload or runtime restart the stop control still clears the stuck turn and cancels the backing agent_runs rows, so the composer unblocks immediately
    • responds { ok, stoppedMessages, agentAbort: { ok, aborted: { orchestrator, native, dbRuns, terminated }, errors[] } }; agentAbort.terminated is true only when every targeted runtime confirms that its exact live run stopped (or no live/persisted run remains). An unreachable runtime or a pending cancellation keeps the outer placeholder-cleanup response successful but returns agentAbort.ok = false, terminated = false, and a runtime error in errors[].
  • /api/chat/audio/[id]

    • GET: authenticated byte-range-capable streaming endpoint for one stored personal chat audio asset owned by the signed-in user; used for both recorded user audio messages and assistant TTS replies in web and Apple chat timelines
  • /api/chat/link-preview

    • GET: signed-in URL metadata extraction (url query param) returning { url, final_url, title, description, image_url, site_name } for chat link preview cards
  • /api/chat/group/directory

    • GET: signed-in list of the user's accessible team-chat rooms as a compact directory payload (used by pickers such as automation result targets and share flows)
  • /api/chat/group/messages

    • GET: load room history from chat_group_messages for the requested room (room=<room-id>), including message_meta.documentReferences for referenced documents/images, message_meta.replyReference for quoted replies, optional message_meta.assistantToolStatuses for in-flight Angela work, and update the current member heartbeat. Large inline agent avatars in message_meta.assistantAgentProfileImageUrl are replaced with stable entity URLs (/api/specialized-agents/:id/profile-image or /api/scheduled-tasks/:id/profile-image) at the shared persistence boundary. Owned agent-audio attachments similarly expose authenticated /api/chat/group/messages/:messageId/attachments/:attachmentIndex URLs instead of returning their bytes in every history poll.
    • GET /api/specialized-agents/:id/profile-image and GET /api/scheduled-tasks/:id/profile-image: authenticated, cacheable delivery for stored inline avatar data. Team Chat references include a content-hash v parameter, allowing the browser to retain matching private responses as immutable cache entries while an avatar change automatically produces a new URL. Unversioned legacy references keep the short revalidation policy. Oversized inline values are decoded only at this delivery boundary and are not copied into new Team Chat messages.
    • GET /api/chat/group/messages/:messageId/attachments/:attachmentIndex: authenticated delivery for one stored agent-audio attachment. Access requires membership in the message's room; the server revalidates the creation-time database provenance, attachment owner, path containment, file identity, single-link state, type, size, and content hash before reading it through a no-follow file handle.
    • POST: persist a human room message for the requested room; used for direct messages in /team-chat, accepts the same attachment JSON shape used by the chat UIs (including optional hidden filePath metadata) plus optional documentReferences[] and optional replyReference, and now also fans out Apple push notifications to the other room participants
  • /api/chat/group/rooms

    • GET: load the signed-in user’s team-chat room list, the member directory including heartbeat presence (is_online, last_seen_at), and a typingByRoom map with short-lived room typing indicators. The list contains active memberships plus every non-deleted public channel; app admins additionally see every private channel, but never other users' direct or group_dm rooms. Each room includes is_member; discovered non-member rooms report unread_count: 0 and the real active member_count. The web and native Apple clients additionally combine this with the enabled specialized-agent catalog for direct specialist DMs and channel invitation search without treating specialists as human room members.
    • POST: create a team-chat room. Channels accept kind: "channel_public"|"channel_private", default to public in the first-party UI, and start with the creator plus explicitly supplied memberUserIds instead of every global user. All new channels/groups start without invited specialists. Passing specializedAgentId opens or creates a signed-in-user direct specialist room backed by a persisted agent:<handle> room default.
    • GET /api/chat/group/rooms/[id]: return the accessible room's active human members, agent_to_agent_enabled, the built-in mainAgent membership/reply mode, and invited specialized agents. Opening a public channel auto-joins a signed-in non-member; app admins may inspect private channels without becoming members. Private channels remain membership-only for regular users.
    • PATCH /api/chat/group/rooms/[id]: channel owners, room admins, and app admins can set visibility: "public"|"private"; direct/group rooms reject visibility changes. clapilot-members (#general) is public by default, but admins may make it private, which suspends auto-join and enables member exclusion; while it is public, memberUserIds changes remain rejected because auto-join includes everyone. Room managers can also replace active human membership with memberUserIds, set agentToAgentEnabled for public/private channels, invite/update/remove the main agent, or independently invite/update/remove specialized agents. The creator remains a member, removed people lose private-room visibility, presence and mention eligibility, and can be re-invited safely.
  • /api/chat/group/rooms/[id]

    • GET: load one accessible room plus human members[], built-in mainAgent, and room-scoped specializedAgents[] invitations; the room and agent records expose mention_only|all_messages reply policy
    • PATCH: update room metadata/member lists, optional visibility: "public"|"private", and optional agentToAgentEnabled; manage the main agent with { action: "inviteMainAgent"|"updateMainAgent"|"removeMainAgent", replyMode? }; or manage specialist invitations with { action: "inviteSpecializedAgent"|"updateSpecializedAgent"|"removeSpecializedAgent", specializedAgentId, replyMode? }. Channel owners, room admins, and app admins can manage channels even when an app admin is not a member. Agent settings and visibility apply only to channel_public and channel_private. The default general channel starts public; admins may make it private to suspend auto-join and manage exclusions, and switching it back to public resumes auto-join for everyone.
  • /api/chat/group/typing

    • POST: refresh or clear the signed-in user’s short-lived room typing heartbeat with { roomId, active }; active=false removes the typing marker immediately
  • /api/chat/group/room-config

    • GET: return the effective main-agent runtime model for the requested team-chat room (room=<room-id>), combining the persisted room override with the current runtime session model lookup, plus mainAgent membership/reply policy and the room-scoped specializedAgents[] invitation list used by team-chat mention autocomplete
    • PATCH: update the requested team-chat room model via { roomId, model }; null or omitted model clears back to the room default, specialist agent:<handle> refs are ignored for team rooms, and the route also best-effort syncs the runtime session model so the next team-chat turn uses the new selection immediately
  • /api/specialized-agents

    • GET: signed-in users receive the enabled shared specialist catalog for direct specialist DMs and room invitation search (id, handle, name, description, optional profile_image_url, enabled, sort_order); team-chat channel mention autocomplete filters this catalog to the active room's invited specialists. Admins receive the full admin-managed catalog including prompt, optional default_model_ref, profile_image_url, channel link state (telegram_channel_enabled, telegram_allow_without_approval, has_telegram_bot_token, telegram_bot_token_hint, whatsapp_channel_enabled, whatsapp_allow_without_approval, whatsapp_number), skill_keys, allowed_tool_names, allowed_auth_resource_keys, include_core_memory_tools, personal_memory_enabled, self_acting_enabled, self_acting_board_id, normalized self_acting_config, allow_runtime_permission_bypass, bundled metadata, and resolved local skill metadata. The non-admin projection does not expose self-acting configuration. Bundled specialists such as agent-orchestrator-supervisor and pet-creator appear in their own UI section, can be edited for allowed fields such as model/prompt, but cannot be deleted or re-keyed.
    • POST: admin-only create path for one shared specialized agent with { handle, name, description?, profileImageUrl?, prompt?, defaultModelRef?, telegramChannelEnabled?, telegramAllowWithoutApproval?, telegramBotToken?, clearTelegramBotToken?, whatsappChannelEnabled?, whatsappAllowWithoutApproval?, skillKeys?, allowedToolNames?, allowedAuthResourceKeys?, includeCoreMemoryTools?, personalMemoryEnabled?, selfActingEnabled?, selfActingConfig?: { intervalMinutes?, instructions?, historyLimit?, teamChatScanEnabled?, reportChannelId?, dailyStandupEnabled?, actorUserId?, dndStart?, dndEnd?, dndTimezone? }, allowRuntimePermissionBypass?, enabled?, sortOrder? } (snake-case aliases, including team_chat_scan_enabled and report_channel_id, are also accepted). reportChannelId names the Team Chat channel the self-acting heartbeat posts status updates to; null/empty disables status posts. personalMemoryEnabled and teamChatScanEnabled default to true; selfActingEnabled defaults to false. Enabling self-acting creates and returns a linked shared Aufgaben board. If profileImageUrl is omitted or invalid, Clapilot stores a deterministic built-in SVG fallback avatar derived from the agent name/handle/description. Uploaded profile pictures may be PNG, JPG/JPEG, or WebP data URLs up to 2 MB. Telegram bot tokens are stored encrypted and are never returned in cleartext. whatsappNumber is not user-entered; the native runtime derives and persists it from the QR-linked WhatsApp account.
    • Runtime session requests carry this setting as specializedAgent.personalMemoryEnabled; changing it alters tool scope and invalidates reusable bridge-session scope hashes.
  • /api/specialized-agents/[id]

    • GET: admin-only fetch for one specialized agent including resolved local skill metadata and the same prompt/tool/auth/approval fields used by the specialist editor.
    • PATCH: admin-only update path for any subset of the same specialist definition fields, including personal-memory and self-acting settings. selfActingConfig is merged key by key over the stored config (send actorUserId: null to clear the acting user). Quiet hours dndStart / dndEnd are "HH:mm" in dndTimezone; omitted keys default to 00:00–08:00 Europe/Berlin and null disables quiet hours. A malformed actorUserId, or one naming an unknown or still-invited user, returns 400 (api.specializedAgents.error.actorUserInvalid); the same validation applies on POST. A transition to enabled self-acting creates or reuses the linked shared board; disabling keeps the board/link but stops heartbeat reconciliation.
    • DELETE: admin-only delete path for user-created specialists; bundled specialists return 400 because shipped system workflows reference them by stable bundled_key
  • /api/specialized-agents/[id]/channels/whatsapp/auth

    • GET: admin-only status for the specialist's dedicated WhatsApp Web session; returns the same shape as /api/agent-runtime/channels/whatsapp/auth, including linked, connected, derived selfE164, authDir, and any active QR login state.
    • POST: admin-only specialist WhatsApp auth actions with { action: "start" | "wait" | "logout", force?, timeoutMs? }; start generates a QR code, wait checks whether the QR scan completed, and logout removes the specialist-scoped WhatsApp auth state. The native runtime stores the derived linked number back on the specialist after successful login.
  • /api/specialized-agents/[id]/channel-approvals

    • GET: admin-only list of Telegram/WhatsApp approval rows scoped to that specialist only; these rows are excluded from the general ClapilotAICore -> Kanäle approval queues.
    • POST: admin-only approval decision with { id, status } where status is approved, denied, or pending. Specialist approvals do not require a Clapilot user mapping; inbound runs execute in the specialist envelope with the specialist prompt, tool allowlist, auth scopes, default model, and permission mode.
  • /api/specialized-agents/profile-icon

    • POST: admin-only image-generation helper for the specialized-agent editor; accepts { name?, handle?, description?, prompt? }, generates a square profile avatar through the configured image-generation runtime using the Clapilot tint palette/style guide, stores it as a generated image asset, and returns { profile_image_url, asset } so the caller can save the URL on the agent. Image-generation transport errors are normalized to a user-facing retry/upload message instead of exposing provider internals.
  • /api/specialized-agents/[id]/embed

    • GET: admin-only fetch for one specialist's external embed deployment; returns the stored deployment plus a placeholder HTML snippet template using the current Clapilot base URL. The deployment includes public_allowed_tool_names[], which is the only tool allowlist used by public website/API-key runs.
    • PUT: admin-only upsert path for { publicSlug, enabled, runtimeAccessMode, publicAllowedToolNames, supportsFileAttachments, allowedOrigins, welcomeMessage }; also ensures a restricted agent_service_principals row exists for that public specialist deployment. runtimeAccessMode is forced to public_safe; publicAllowedToolNames is filtered to public-safe tools and is independent from the specialist's internal allowed_tool_names, Core Memory, auth scopes, and runtime-bypass settings.
  • /api/specialized-agents/[id]/embed/keys

    • POST: admin-only create path for one publishable embed API key tied to the specialist deployment; accepts optional { label, expiresAt } and returns { api_key, plain_text_key, snippet, deployment }, where plain_text_key is shown only once. Each key also owns a stable session_id mapping so external callers can omit a session header and still continue the key-bound specialist session.
  • /api/specialized-agents/[id]/embed/keys/[keyId]

    • DELETE: admin-only revoke path for one specialist embed API key
  • /api/specialized-agents/embed-abuse

    • GET: admin-only overview of blocked/rate-limited public embed traffic; accepts optional agentId and limit (default 80) and returns recent abuse events plus per-deployment counters for the specialist embed settings UI
  • specialized-agent hub snapshots

    • schema: { kind: "clapilot.specialized-agent", schema_version: 1, handle, name, description, prompt, default_model_ref, skill_keys, allowed_tool_names, allowed_auth_resource_keys, include_core_memory_tools }
    • intentionally omitted: public embed config, API keys, secrets, service principals, and runtime permission bypass
  • /api/public/agents/[slug]

    • OPTIONS: CORS preflight for public/embed agent metadata requests
    • GET: public metadata endpoint for one enabled embedded specialist; requires X-Clapilot-Embed-Key, checks the deployment origin allowlist against the incoming Origin, and returns { agent, deployment } for widget bootstrapping without a normal user session, including deployment flags such as runtime_access_mode and supports_file_attachments
  • /api/public/agents/[slug]/messages

    • OPTIONS: CORS preflight for public/embed message requests
    • POST: public message endpoint for one enabled embedded specialist; requires X-Clapilot-Embed-Key, checks the same origin allowlist, accepts { sessionId?, message }, runs the specialist in isolated public-embed scope, and returns { sessionId, agent, reply, run } so a website widget can keep lightweight continuity without using the normal personal/team chat routes. If sessionId is omitted, the endpoint uses the API key's stored session_id mapping instead of creating a new random session.
    • the endpoint now also applies server-side safety and abuse guards before any model run:
      • obvious prompt-injection attempts targeting hidden instructions/system prompts are blocked
      • obvious requests for secrets, credentials, or tokens are blocked
      • obvious requests for internal/private workspace activity are blocked
      • per deployment + IP limits cap both the number of new sessions per 24 hours and the message frequency per 10 minutes / 24 hours
    • blocked public requests return a safe assistant reply instead of executing the specialist, and rate-limited JSON responses include Retry-After
  • /api/v1/models

    • GET: OpenAI-compatible model list for a generated specialist embed API key supplied as Authorization: Bearer <key> or X-Clapilot-Embed-Key. The key is scoped to one enabled specialist deployment, so the response lists only aliases for that specialist such as the public slug, handle, agent:<handle>, and clapilot/<slug>. /v1/models is also supported as a compatibility alias for clients that expect the standard OpenAI path at the domain root.
  • /api/v1/chat/completions

    • POST: OpenAI-compatible chat-completions endpoint for generated specialist embed API keys. Accepts normal { model, messages, stream?, user? } payloads and returns OpenAI-style chat.completion JSON or text/event-stream chunks.
    • model must match one of the aliases returned by /api/v1/models; the API key determines the underlying specialist, prompt, public embed tool allowlist, service principal, and public-safe runtime envelope.
    • /v1/chat/completions is also supported as a compatibility alias for clients that expect the standard OpenAI path at the domain root.
    • server-side execution defaults to the public embed guardrails: no authenticated user context, only public_allowed_tool_names[] from the embed deployment, no fallback to internal specialist tools, no stored auth scopes, no Codex/Claude subscription bridge routing, prompt/secret/internal-data safety checks, and deployment/IP rate limits. An empty public allowlist means exactly zero tools. X-Clapilot-Session-Id or user may override the lightweight session key for rate-limit continuity; when both are omitted, the API key's stored session_id mapping is used so the same key continues the same specialist session. Safety checks inspect the latest user message only, so upstream system/developer instructions that mention secrets or API keys do not trigger the public-safe canned reply by themselves. Non-streaming responses include clapilot_session_id, and both streaming and non-streaming responses expose X-Clapilot-Session-Id.
    • publishable embed/API keys cannot switch to the normal internal specialist runtime. Full internal specialist capability requires an authenticated signed-in Clapilot route, not these public endpoints.
    • vision input supports OpenAI-style image_url content parts with data:image/...;base64,... values and raw base64 image fields such as image_base64, base64, or data when paired with type: "input_image" / type: "image".
    • when an allowed public specialist uses images_generate, generated images are stored under the embed service principal and returned as tokenized public links. Non-streaming responses include these links in choices[0].message.content and in the extension field clapilot_images[].
    • the specialist settings dialog shows these exact OpenAI-compatible paths in Externe Kanaele -> Web / UI Embed -> API Keys alongside the generated key. The API uses /api/v1/chat/completions as the stable path and selects the specialist through the OpenAI model field.
  • /api/public/generated-images/:id?token=...

    • GET: public-safe generated image download used by public specialist/API and Team Chat image output. The image id alone is not enough; the per-image token must match the hash stored in the asset metadata.
  • /api/public/generated-videos/:id?token=...

    • GET: public-safe ready-video download used by Team Chat. The raw token is returned only in the generated link; its SHA-256 hash and room scope are stored in generated_videos.metadata.public_access.
  • /api/specialized-agents/overview

    • GET: signed-in lightweight overview for the end-user-friendly Agenten segment inside /geplante-aufgaben?view=agents; returns { summary, agents, running, is_admin } with per-agent counts, optional profile_image_url, last activity, running-session counters, active specialist sessions, and per-agent recent_runs[] history previews for expandable row drill-down. Non-admin responses scope running/history entries to the caller’s own personal specialist runs plus accessible team-chat rooms instead of exposing global chat content. The workspace-global activity bundle (sessions, tasks, run previews, scheduled-task references) and the agent list are cached in-process for 5 seconds with in-flight coalescing, so concurrent tabs and clients share one query set; specialist create/update/delete invalidate the cache and ?fresh=1 bypasses it for the UI reload right after a mutation
  • /api/agent-runtime/memory/dreams

    • GET: admin-only Dream history. New inputRefs entries distinguish kind=memory_source (original user-source ledger excerpt) from kind=agent_memory and include progressVersion=1, contentHash, startOffset, endOffset, and totalChars for the exact source range supplied to the provider. Older entries may omit these fields and do not establish consumption progress.
    • POST: run the existing audience-partitioned Dream pipeline. Original user-message/manual excerpts are eligible even without approved projections; generated Dreams and fully consumed unchanged source content are excluded. Failed/rolled-back source ranges remain retryable. Existing model selection, approval policy, graph projection and Wiki proposal boundaries remain in force.
  • /api/agent-runtime/tool-catalog

    • GET: admin-only native tool-catalog endpoint for the specialized-agent settings UI; returns the current ClapilotAICore tool inventory with normalized { name, description, category, parameterNames, requiredParameterNames, isCore, isMainAgentOnly, selectableForSpecialists, selectableReason }
    • the response also includes core_tool_names, specialist_blocked_tool_names, and selectable_tools so the admin UI can render a searchable picker instead of a freeform allowlist textarea
  • /api/agent-runtime/transport-status

    • GET: admin-only monitor for the app → clapilot-agent internal transport. Returns { ok, status, probe }: status holds the in-process counters kept by the shared runtime transport (baseUrl, secretConfigured, health = unknown|healthy|degraded|unreachable, totals with requests/successes/transportFailures/httpServerErrors/retries/recoveredAfterRetry, consecutiveTransportFailures, lastSuccessAt, lastTransportFailureAt, lastTransportFailure with kind/code/method/path/attempts/elapsedMs/message, failuresByKind, the last 20 recentFailures, and the effective retry config). With ?probe=1 (optional timeoutMs, max 30000) it also runs a live probe: probe.dns (hostname lookup with addresses and duration; skipped for IP/localhost targets), probe.http (GET /health without retries: status, duration, classified kind/code on failure, parsed body) and a one-line summary. ok reflects the probe when requested, otherwise health !== "unreachable".
  • /api/agent-runtime/auth-catalog

    • GET: admin-only specialized-agent auth/access catalog endpoint; returns { auth_resources, default_selected_keys } for the current provider/runtime auth scopes (for example image generation, TTS/STT, OpenAI, Gemini, GitHub, Brave, and Perplexity) so the settings UI can render an explicit access picker instead of implicit global secret access
  • /api/chat/sessions

    • GET: list personal chat sessions for the signed-in user, with the main session first and other pinned sessions sorted ahead of the remaining recents
    • every session response here and under /api/chat/sessions/[id] (including ?recent=true and /reset) returns titles that are still the stored defaults (Hauptchat, Neue Session) in the request UI language (ui_language=de|en|it or the clapilot_ui_language cookie); custom and auto-generated titles are returned unchanged
    • POST: with ensureScope=<key> (e.g. video-studio), get-or-create a stable, non-main feature workspace session keyed per (scope, user) with a deterministic chat-<scope>-<user> id (kept separate from the main session); with createFresh=true, create a new custom personal session; without either, resolve or create the user’s main personal session
  • /api/chat/sessions/[id]

    • GET: load one personal session
    • PATCH: update { title?, model?, isPinned? }; setting title is treated as a manual rename and prevents later auto-title overwrites, while isPinned toggles persisted session pinning for custom personal sessions
    • DELETE: delete one custom personal session; the main personal session is protected and returns a validation error instead of being removed
    • POST /api/chat/sessions/[id]/reset: clear one personal session's visible message history, rotate its runtime session_user, and best-effort reset the attached agent runtime session so the next turn starts with fresh context while keeping the chat session itself
  • /api/chat/models

    • GET: returns signed-in user's available chat models as { models }
    • model entries include runtimeProvider and supportsSteering; the web chat uses supportsSteering to show queued-message direct steering only for currently steerable models such as OpenAI-Codex, Claude subscription rows, or non-specialized models when the active core is embedded_pi (the internal adapter behind the Agent Orchestrator clapilot-code harness)
  • /api/chat/live/session

    • returns provider-specific live connection metadata based on the configured Realtime provider/model selection
    • OpenAI API responses use the GA Realtime API: /realtime/client_secrets for the short-lived client_secret and /realtime/calls for the WebRTC SDP handshake, with realtime_api: "ga" in the response; OpenAI-family responses also include websocket_url so native Apple clients connect to the configured Realtime provider URL instead of hard-coding OpenAI
    • OpenAI-compatible or Azure responses may return realtime_api: "beta" when routed through their provider-specific legacy-compatible Realtime surface
    • Google Gemini responses include transport: "gemini_websocket" plus websocket_url, access_token, instructions, and Gemini tool declarations
  • /api/chat/live/relay/session

    • POST: authenticated Apple Watch live entrypoint; creates the provider Realtime session server-side, opens the upstream provider WebSocket from Clapilot, and returns transport: "clapilot_relay" plus relay_session_id
    • GET /api/chat/live/relay/[sessionId]/events: authenticated SSE stream of upstream Realtime JSON events back to the watch
    • POST /api/chat/live/relay/[sessionId]/input: accepts one Realtime JSON event from the watch, including input_audio_buffer.append, tool responses, and cancellation events, then forwards it to the upstream provider WebSocket
    • DELETE /api/chat/live/relay/[sessionId]: closes the server-held upstream WebSocket; relay sessions also expire automatically after inactivity
  • /api/chat/live/tools

    • live tool execution may return triggerReload, refreshTopic, uiActions[], and mutationEventId
    • includes navigate_user_to_page, which resolves validated internal Clapilot routes and emits navigation.open so chat/live agents can move the user directly into pages like E-Mail detail, calendar event, document folder (/dokumente?folder=:folder_id), documents preview/editor, tasks, Mandanten, Website Canvas, or other internal app destinations
    • includes google_meet for Live Voice so spoken sessions can join, inspect, speak in, transcribe, summarize, and leave managed Google Meet browser participants. Join first validates the meeting space through the Meet REST API with the dedicated Agent Google account. The session payload reports accountEmail plus browserAccountMode: signed_in when the persistent managed-browser profile already has Google login cookies, or guest_fallback when OAuth access was verified but Google browser login is still absent. Successful join automatically requests captions and starts the server-side voice-to-voice bridge when enabled and remote Meet audio is available
    • the live tool catalog now includes exec_command for direct shell/bash execution inside the native runtime workspace
    • the live tool catalog now includes mini_apps_* and widgets_* operations for listing, creating, updating, and filling Widgets from chat/live-agent flows
  • widget tool calls stay structured-only and never accept raw HTML; widget_definition can be omitted on create so Clapilot infers a native layout from latest_data

    • the live tool catalog includes notizen_* operations for folder, note, and page management inside the Notizen module, including notizen_duplicate_local for read-only reMarkable imports
    • the live and native tool catalogs now include faxes_list, faxes_get, faxes_send, faxes_retry, and faxes_cancel for the shared Call Agent fax workflow
    • the native runtime tool proxy mirrors calendar CRUD through calendar_list_events, calendar_get_event, calendar_create_event, calendar_update_event, and calendar_delete_event; list results defensively collapse provider duplicates by canonical calendar, external event ID, and instance start time before briefings or agents receive them, and additionally consolidate semantically identical deadline entries (frist entries or deadline-worded titles that share the same invoice, quote, order, or dunning number such as Rechnung 270826 less than 24 hours apart, or the same normalized title at the same start; a conflicting document reference, entity reference, amount, mandant_id, or deadline kind always keeps entries apart, so installments of one invoice (the amount is read from title, description, and source_label) and the same generic deadline for two customers stay listed, and a Skonto date never merges with the net payment date of the same invoice; equal titles without a shared document number additionally need a matching source_label; case, contract, and customer numbers only tell equal titles apart and never merge differently titled deadlines, including the spaced German Aktenzeichen notation (Aktenzeichen 12 O 345/26), while a document number is preferred over an entity number when a title names both; a bare year or date after a keyword such as Vertrag 2026 or Auftrag 15.09.2026 is not treated as a reference; a consolidated group never spans 24 hours or more, so an extended deadline on a later day stays listed) into one event that carries the merged ids in consolidated_entry_ids, so a Morning Briefing lists a payment deadline once even when e-mail automation, document analysis, and an agent each created their own entry; calendar_get_event recomputes and returns the same consolidated_entry_ids, and calendar_update_event and calendar_delete_event accept consolidated_entry_ids: string[] (max 20) to apply the change to every merged duplicate, returning consolidated_updated_ids/consolidated_failed_ids and deleted_ids respectively; calendar_create_event accepts optional calendar_name to create directly in a matching Apple/iCloud calendar and returns an error when the Apple calendar name is missing or ambiguous; create/update accept attendees: string[] and send Google invitations via sendUpdates=all (an empty update list removes all guests), and also accept create_google_meet or conference_type="google_meet" to create/add a Google Meet link. Agent-created Google events prefer the dedicated Agent Google account configured under App Connections and fall back to the user's connected account. Google updates use events.patch with stored ETag/If-Match, returning conflicts instead of overwriting newer remote changes.
    • the native runtime tool proxy mirrors Aufgaben board/task operations through aufgaben_list_boards, aufgaben_create_board, aufgaben_list_statuses, aufgaben_manage_statuses, aufgaben_list_tasks, aufgaben_get_task, aufgaben_create_task, aufgaben_move_status, aufgaben_assign_task, aufgaben_update_task, and aufgaben_delete_task; status filters and mutations accept the workspace's configured keys, invalid keys return the valid list, board/task reads are scoped to shared boards plus the authenticated user's private boards, board listing includes the preset templates akquise, marketing, and finanzen, and task create/update tools accept the structured CRM/follow-up/deal fields used by Akquise and partner workflows; aufgaben_create_task additionally accepts idempotency_key and allow_duplicate, treats the persisted task id as the success signal (taskId, persisted: true, detailsUnavailable: true when the post-insert reload failed), and returns alreadyExists: true with duplicateOf/duplicateReason (idempotency_key or title_match) instead of creating a semantic duplicate
    • the live/native tool catalogs include cases_list, cases_get, cases_create, cases_update, cases_delete, cases_link_entity, cases_unlink_entity, cases_add_communication, and cases_add_key_date for the optional bundled Cases module
    • the native runtime tool proxy mirrors mailbox and draft flows through emails_list_messages, emails_get_message, emails_apply_message_action, emails_list_drafts, emails_get_draft, emails_create_draft, emails_update_draft, emails_delete_draft, and emails_send_draft; personal emails_list_messages searches are global across available mail folders unless the tool call supplies a folder, which keeps folder-scoped searches explicit, and emails_apply_message_action uses the same action routes as the E-Mail UI for read/unread/move/archive/delete actions
    • the live/native Website Canvas bridge exposes website_sync_repo so chat/live agents can refresh the active repo clone to the latest remote fast-forward state
    • long-running website_ensure_session and website_apply_change calls use POST /api/modules/website-canvas/api/operation/{ensure|apply} and return an operation ID with HTTP 202; GET /api/modules/website-canvas/api/operation/:operationId provides idempotent queued/running/succeeded/failed status, optionally waiting up to 20 seconds through waitMs, and an unknown ID is explicitly not_started. Native provider loops allow up to 60 all-passive operation polls beyond their normal mutation-step budget, covering the Website Canvas 20-minute execution window without permitting unbounded polling. One checkout-scoped queue serializes async operations with direct ensure/sync/chat/commit/push mutations against the shared repo checkout. Explicit idempotency keys are additionally fingerprinted with the authenticated caller and normalized payload. forceNewSession discards the shared checkout and re-clones it fresh; it never creates a second parallel checkout. Completed records stay queryable for eight hours. Failed operations release their keys immediately for retry; successful operations retain a five-minute same-key deduplication window.
    • the native/live tool surfaces include copilot_ui_render; it returns a structured copilot.ui.render action and persists normalized assistant UI payloads under message_meta.assistantUiElements so web chat, floating chat, and the Apple native chat client can render CopilotKit-style response UI inline. In reverse-mirrored Telegram rooms, the persisted Team Chat payload remains canonical and is projected to Telegram card text with signed inline choices[] callbacks and public HTTP(S) action buttons
    • agent action approvals reuse the same element: a card with tone: "warning", id: "action-approval-<approvalId>", and an optional approval object (approvalId uuid, status one of pending|approved|executing|executed|failed|rejected|expired, effect one of outbound|delete|purchase|publish, toolName, details[] of { key, value } strings capped at 16 entries and 1,000 characters per value, expiresAt). src/lib/copilotkit-ui.ts drops an invalid approval object and keeps the fallback title. Web chat, Team Chat, and the floating chat render it as an approval card (src/components/chat-action-approval-card.tsx). The card re-reads GET /api/agent-runtime/action-approvals/{id} on mount while the status is not final, and decides through POST /api/agent-runtime/action-approvals/{id}/decision rather than a chat turn. The chat page also shows a pending-approvals banner backed by GET /api/agent-runtime/action-approvals?status=pending.
    • the native runtime tool proxy mirrors document workflow operations through documents_create, documents_list_folders, documents_create_folder, documents_update_folder, documents_delete_folder, documents_update, documents_move, documents_delete, documents_create_share, and documents_revoke_share; document reads are scoped to shared documents plus the authenticated user's private documents, and documents_update accepts extracted tax fields plus related_mandant_ids for multi-party document assignment. documents_create can create Word/Excel/Markdown records directly in Root-Dokumente even when the stored file_path ends up under _inbox/..., and accepts template_key="vollmacht", mandant_id, plus optional matter, deadline, and location to generate a Mandant-linked standard power-of-attorney Word draft through Word Editor
    • the live/native tool surfaces now also mirror physical-post workflows through postal_mail_list, postal_mail_get, postal_mail_send, and postal_mail_refresh
    • request body also accepts optional userUtterance so the backend can reuse the last spoken user request during fallback delegation
    • lookup-oriented direct-tool misses can auto-fallback server-side into clapilot_delegate before the result is returned to Realtime
    • delegated execution routes through the native clapilot-agent runtime backend
    • current structured UI mutation examples: excel.cells.updated, excel.sheet.updated, word.document.updated
  • /api/ui-mutation-events

    • GET: authenticated SSE stream of persisted user-scoped UI mutation events emitted by live tools, native tool proxy executions, and chat context actions
    • GET ?format=json&since=<cursor>&topic=<topic>: bounded JSON cursor read for native clients; returns events[] plus the next cursor, supports since=latest initialization without replay, and filters server-side so an open native Aufgaben view does not repeatedly download the full task collection
    • each SSE item includes a stable id / mutationEventId, optional topic, triggerReload, actions[], and createdAt
    • the authenticated web app shell consumes the SSE stream, while Apple clients consume the lightweight cursor form and refetch only affected task IDs, so visible pages can react to agent-driven mutations without a hard refresh
  • /api/documents/[id]/share

    • GET: return the active public share metadata for one document, if present
    • POST: create or reuse a cryptographically random public share link for one document; accepts optional expires_in (24h, 7d, permanent) or an absolute expiry timestamp
    • DELETE: revoke the active public share link for one document
  • /share/[hash]

    • GET: public file delivery endpoint for an active document share; validates expiry and active state, increments access_count, and applies a simple per-IP rate limit before streaming the original file without requiring login

Physische Post (E-POST)

  • /api/postal-mail
    • GET: list recent physical-post jobs from the shared E-POSTBUSINESS outbound queue; accepts limit (1-100, default 20)
  • /api/postal-mail/[id]
    • GET: return one physical-post job including persisted event history
  • /api/postal-mail/send
    • POST: send an existing PDF document as physical post; V1 rejects non-PDF inputs and stores provider status snapshots locally
    • registered_letter accepts exactly the provider options Einschreiben, Einwurf Einschreiben, Einschreiben Rückschein (common synonyms are normalized; unknown values return invalidRegisteredLetter)
    • country is only for international mail (German uppercase ISO 3166-1 country names, e.g. ÖSTERREICH); domestic values like DE/Deutschland are dropped before submission
    • test-mode submissions attach the configured epost_test_email as provider testEMail, so the rendered test letter is mailed back instead of printed
  • /api/postal-mail/[id]/refresh
    • POST: refresh one physical-post job by polling the provider status endpoint
  • /api/internal/postal-mail/sync
    • POST: internal endpoint (agent-secret header) used by the preinstalled Postal Mail Status Sync system automation to refresh all open physical-post jobs (statuses submitted, processing, in_print_center) in batches
  • /api/postal-mail/admin/sms-request
    • POST: admin-only bootstrap step to request the E-POST SMS code
  • /api/postal-mail/admin/set-password
    • POST: admin-only bootstrap step to set the provider password and persist the returned secret into shared app settings
  • /api/postal-mail/admin/test-connection
    • POST: admin-only provider connectivity check using the current shared E-POST settings
  • /api/calendar
    • GET: returns the signed-in user's entries for the requested from / to interval. Entry colors are normalized as lowercase six-digit hex values without palette quantization and are resolved through the same display-color helper used by the Kalender server-rendered initial state and installed web app.
    • GET /api/calendar/calendars: lists writable calendar targets for the create-entry modal, including the local Clapilot calendar and connected Apple/iCloud CalDAV calendars
    • POST /api/calendar/ai-prefill: authenticated Kalender modal helper. Sends the natural-language description to ClapilotAICore as a tool-free structured extraction run and returns { prefill } with title, description, location, optional calendarName, start/end, and all-day state. The endpoint does not use deterministic client parsing and returns a validation error instead of inventing missing required details.
    • POST: creates a calendar entry; accepts optional calendar_name to write the event into a matching Apple/iCloud calendar first, mirroring the native calendar_create_event tool behavior; accepts create_google_meet / conference_type="google_meet" to create the event in Google Calendar with generated conferenceData and return google_conference_url plus provider-agnostic conference_url.
  • /api/calendar/ical
    • GET: exports the authenticated user's calendar entries for the requested from / to range as a downloadable text/calendar .ics file
    • POST: imports iCalendar data from JSON { content }, JSON { url }, raw text/calendar, or multipart .ics upload; webcal:// feed URLs are normalized to https:// before server-side fetches; recurring events are expanded into bounded local entries, deduped by iCal UID through external_id + sync_source='ical', and stored as mirrored calendar rows
  • /api/calendar/ical/subscriptions
    • GET: lists the signed-in user's saved iCal subscriptions and their sync status
    • POST: creates or updates a saved iCal feed subscription with name, feed_url, optional color, optional context_label, enabled, and sync_interval_minutes; webcal:// feed URLs are normalized to https://; iCal subscriptions are read-only mirrors (sync_mode = read_only) and feeds are refreshed by the iCal subscription poller or when the Kalender page opens a due range
  • /api/calendar/ical/subscriptions/[id]
    • PATCH: updates a saved iCal subscription
    • DELETE: removes the subscription and deletes its mirrored imported entries
  • /api/calendar/ical/subscriptions/[id]/sync
    • POST: immediately syncs one saved iCal subscription for the signed-in user
  • /api/calendar/ical/subscriptions/sync
    • POST: syncs the signed-in user's enabled subscriptions, with due_only defaulting to true
  • /api/internal/calendar/ical-subscriptions/sync-due
    • POST: internal-only endpoint guarded by x-clapilot-agent-secret; the container iCal poller calls it to refresh due saved iCal subscriptions in the background
  • /api/integrations/apple/status
    • GET: returns the signed-in user's Apple iCloud connection status and service toggles for CalDAV calendar, CardDAV contacts, and iCloud Mail, including the optional mail_address
    • PATCH: updates Apple iCloud service toggles (calendar_enabled, contacts_enabled, mail_enabled)
  • /api/integrations/apple/connect
    • POST: connects or updates Apple iCloud using apple_id, an app-specific password, optional display_name, optional mail_address, and service toggles; Clapilot validates CalDAV/CardDAV discovery before saving when those services are enabled
    • DELETE: disconnects the signed-in user's Apple iCloud credentials
  • /api/integrations/apple/calendar/sync
    • POST: syncs Apple iCloud CalDAV calendars into local calendar_entries with sync_source = apple; ordinary non-recurring Apple events can be edited, deleted, or moved between local Clapilot and Apple/iCloud calendar targets from Clapilot and written back via CalDAV
  • /api/integrations/apple/contacts
    • GET: reads Apple iCloud CardDAV contacts for the signed-in user, with optional q and max query parameters
  • /settings/contacts
    • user-facing Contacts settings entry for file-based contact imports (VCF upload into mandanten); provider contact sync is not offered here — it lives on the Apple iCloud, Google Workspace, and Microsoft 365 connection cards under Settings -> App Verbindungen
  • /api/contacts/import/[provider]
    • POST: syncs contacts from the signed-in user's connected account into mandanten for provider in apple | google | microsoft; triggered from the App Verbindungen connection cards (explicit sync button, plus automatically when the contacts service toggle is switched on); the provider's contacts service must be connected and enabled
    • upserts by (sync_source, external_id): new provider contacts create rows, changed contacts update the previously synced row, unchanged contacts are skipped; a provider contact whose email matches an existing untracked mandanten row adopts that row instead of duplicating it (existing field values win, empty fields are filled)
    • responds with { provider, parsed, created, updated, skipped, failed, results[] }, mirroring the VCF import result shape plus the updated counter; contact syncs are capped at 1000 contacts per run and never delete local rows when a contact disappears at the provider
  • /api/calendar/live
    • GET: authenticated SSE stream backed by Postgres LISTEN/NOTIFY on calendar_entries
    • accepts from / to so only changes intersecting the visible calendar range are forwarded
    • intended for background refetch/diff animation of created, moved, updated, or deleted events without a hard refresh
  • /api/calendar/[id]
    • calendar entries now support optional deadline metadata fields source_type, source_origin, source_label, detected_date, and mandant_id
    • calendar payloads include the unified sync fields external_id, sync_source, sync_status, and linked_entities; Microsoft/Outlook imports are exposed as sync_source='outlook' mirrored records, and iCal imports use sync_source='ical'
    • calendar payloads include provider-agnostic conference_url and conference_provider fields for online meetings; the Kalender UI also extracts Teams/Meet/Zoom/Webex links from location and description as a fallback for Apple/iCal imports
    • entries with source_type = frist render with deadline countdowns, critical badges, and a deadline-only filter in the Kalender UI
    • calendar payloads also expose optional mandant_id so Mandant-linked events can round-trip through the customer timeline and agent-created calendar flows
    • PATCH accepts calendar_name for local/Apple events to move the entry between the local Clapilot calendar and a matching Apple/iCloud calendar; it also accepts create_google_meet / conference_type="google_meet" for existing Google-synced events and stores the generated Meet link in google_conference_url and conference_url.
  • /api/generated-images/generate
    • Optional source_image_paths (alias sourceImagePaths) accepts an ordered array of up to four workspace image paths and overrides the singular source_image_path. Malformed lists are rejected. Paths use the existing workspace import validation; all sources are imported and passed together to editing. Existing reference_image_ids fill the remaining slots up to four total. The response echoes only forwarded source_image_paths. Social Media selects up to three for compatibility across providers.
    • Optional reference_image_ids (alias referenceImageIds) accepts up to 4 owner-owned asset IDs (3 when source_image_path is also given, because the imported source occupies the first of the four provider image slots) and routes through the edit pipeline as visual references, after the imported source_image_path image when present. IDs are trimmed, de-duplicated, and capped; the response echoes only the IDs the provider actually forwarded (from the asset's source_asset_ids metadata), so a reference dropped by a provider cap is not reported as applied. Invalid lists return 400 and missing/foreign references return 404.
    • POST: generate a persisted image asset via the default entry in app_settings.media_model_catalog.image when that catalog is non-empty, otherwise via the global ClapilotAICore image-generation provider/model default. Callers may pass optional provider_slug plus model to select one exact enabled runtime provider without fallback; explicit per-request values are not replaced by the catalog. Recognized model-family overrides without provider_slug remain supported for compatibility. size accepts 1024x1024, 1536x1024, or 1024x1536, and quality=high requests 2k output from xAI. Omitted or auto values remain provider-controlled. Returns asset metadata plus a renderable markdown image reference. Agent tool calls that include source_image_path are treated as source-image edits for compatibility so local reference images do not degrade into text-only generation.
  • /api/generated-images/edit
    • Optional reference_image_ids (alias referenceImageIds) accepts up to 3 extra owner-owned asset IDs appended after the base image; IDs are trimmed, de-duplicated, exclude the base, and are capped. Optional mask_image_id (alias maskImageId) identifies an owner-owned PNG mask asset: transparent pixels are editable, sent as the OpenAI /images/edits mask field or as a guidance image for Codex/Gemini. xAI accepts three image inputs in total and keeps the mask ahead of trailing references. The response echoes the mask ID and only the reference IDs the provider actually forwarded (from the asset's source_asset_ids metadata); invalid reference lists return 400 and missing/foreign assets return 404.
    • POST: edit a persisted, uploaded, current chat-attachment, selected Notizen, or workspace-local source_image_path image source via an edit-capable ClapilotAICore image provider and return a new persisted asset. Optional provider_slug plus model selects one exact provider without fallback: a disabled or missing provider returns a localized providerUnavailable error, an OpenAI-compatible provider whose /images/edits endpoint answers 404/405/501 returns providerEditUnsupported, and a catalog model curated as text-to-image only returns modelEditUnsupported before any upstream call. Workspace paths are constrained to the configured workspace root, imported as source assets, and recorded in generated-image metadata. Without an explicit provider, the catalog default is used when its imageOperations attribute allows edits, otherwise the first edit-capable catalog entry; only uncurated defaults (no attribute) that look like OpenAI-compatible text-to-image-only models such as Ideogram are bypassed in favor of OpenAI-Codex, OpenAI API, or Gemini image routing. Every call is logged in agent_model_request_logs with request_kind = image, endpoint = image.edit, the effective provider, and metadata.base_url. When the selected provider is OpenAI-Codex, Clapilot routes through chatgpt.com/backend-api/codex/responses with the hosted image_generation tool and stored Codex OAuth instead of requiring OPENAI_API_KEY.
  • /api/generated-images/import
    • POST: import an uploaded/external image (up to 20 MB) as a user-owned generated-image asset and return asset metadata plus renderable markdown; used when chats and tools need a persisted asset id for a non-generated source image. Optional multipart field import_source (slug of a-z, 0-9, _, -, up to 64 characters, case-folded to lowercase; omitted or empty defaults to drop) is stored as metadata.import_source and echoed back as import_source in the response so callers such as the Image Playground can tag throwaway masks and references (image-playground-mask, image-playground-reference, image-playground-upload) for later listing filters or cleanup; a malformed tag returns 400 instead of being silently coerced
  • /api/generated-images/[id]
    • GET: stream the authenticated user-owned generated image binary
  • /api/generated-videos/generate
    • POST: start a persisted text-to-video or image-to-video job through the configured AI media video provider (media_generation_provider_configs). In addition to prompt, callers can pass a user-owned image_id/source_image_id or workspace-local source_image_path; workspace sources are imported into generated_images, ownership is checked, and the source asset lineage is stored in generated_videos.metadata. Image-to-video is implemented for every video provider type: xAI Grok sends the image to /videos/generations as a base64 data URI in image.url (the source aspect ratio is preserved by default); Gemini Veo sends it inline as instances[].image.bytesBase64Encoded; Kie sends a tokenized public URL (input.image_urls for Kling, imageUrls with FIRST_AND_LAST_FRAMES_2_VIDEO for Veo), with an optional last frame as the second entry; OpenAI-compatible providers send it as the multipart input_reference file. OpenAI-compatible video providers additionally accept reference-based generation: last_frame_image_id/last_frame_path, reference_image_ids/reference_image_paths (9 images max including the start image), reference_video_paths (3 max), reference_audio_paths (3 max), and the fit (auto/cover/contain/stretch), ref_detail (match/max) and use_video_audio controls. Files are sent as repeated multipart fields (ref_image, ref_video, ref_audio, last_frame); workspace-local reference paths are constrained to the workspace root. Caps are enforced server-side before the request, and reference inputs sent to a non-OpenAI-compatible provider are rejected. Under fit=auto the configured size acts as a pixel budget rather than the literal output geometry, so the response size field is not authoritative. When no image is supplied and the configured default is the image-only grok-imagine-video-1.5, Clapilot selects the configured text-capable grok-imagine-video model instead. Returns the generated-video asset metadata, provider response, and an initial poll result. Async jobs stay generating until status polling downloads the provider output.
  • /api/generated-videos/[id]/status
    • GET: read and, when still generating, poll a generated-video job by id. Any authenticated user may poll any job so workspace-global Video Studio storyboards reconcile for non-creators. Ready jobs include videoUrl and videoMarkdown. Chat-started async jobs also create a hidden one-shot wake-up tied to the originating web/Team Chat target; it silently reschedules status checks and posts the terminal result without requiring another user message.
  • /api/generated-videos/[id]
    • GET: stream a generated video binary by id once status is ready. Any authenticated user may fetch any generated video so shared Video Studio storyboards render for non-creators; creation still stamps the requesting user as owner_user_id. Markdown links to this route render as inline, controllable video players in assistant chat messages.
  • /api/integrations/google/oauth/start
    • POST: starts Google OAuth. Accepts scope_presets / scopes, optional account_type="user"|"agent", optional redirect_path, and optional include_granted_scopes (default false). Workspace connections keep historical Google grants isolated so previously approved YouTube and Drive permissions are not merged into an incompatible consent request. The dedicated agent preset uses Calendar, Gmail, Drive, and Meet-space-read scopes. Other presets include gmail, meet, youtube_live_chat, and youtube_live_chat_write.
  • /api/integrations/google/oauth
  • /api/integrations/google/oauth/complete
  • /api/integrations/google/oauth/status
    • GET: returns connection state, granted scopes, connected account info, and per-service enablement flags
    • PATCH: updates one or more per-user Google service toggles via service_settings
  • /api/integrations/google/meet/browser-login
    • GET: returns the authenticated user's active managed Agent Google browser-login status; screenshot=1 returns the current managed-browser viewport as a non-cached PNG
    • POST: starts or controls the one-time managed-browser Google sign-in with action=start|click|type|press|refresh|close. The route never returns browser cookies and keeps the resulting Google web session in the account-isolated persistent Meet profile
  • /api/integrations/google/calendar/sync
  • /api/integrations/google/contacts
  • /api/integrations/google/docs-sheets
  • /api/integrations/google/drive/sync
    • POST: imports the newest Drive files (max_files, default 50, max 200) into Dokumente immediately (used by onboarding); accepts optional account_type="agent" to use the dedicated Agent Google identity and its isolated Drive subtree. The complete Drive tree is mirrored by the background worker instead.
  • /api/integrations/google/drive/items
    • GET: browses the signed-in user's own Google Drive metadata mirror (modules:api:read). Query: account_type=user|agent (default user, falls back to the first connected account), folder_id (omit for the Drive root; the root also lists items whose parent is not visible, such as shared-with-me files), search (case-insensitive name search across the whole account, ignores folder_id), sort=name_asc|name_desc|modified_desc|modified_asc|size_desc|size_asc, folders_only=1 (only folders, e.g. for folder pickers), limit (1-500, default 100), offset, summary=1 (return only accounts). Response: { accounts, account, folder, breadcrumbs, search, items, pagination: { limit, offset, total, next_offset } }. accounts[] carries account_type, email, root_folder_id, sync_status (pending|listing|ready|error), listing_completed_at, last_synced_at, last_error, folder_count, file_count, imported_count. items[] are folders first with id, name, mime_type, is_folder, size, modified_time, web_view_link, importable, import_status, import_error, child_count (folders), parent_id, parent_name, and document (id, titel, file_path, mime_type, file_size, created_at) when the file is already in Dokumente and visible to the caller. breadcrumbs excludes the root; an unknown folder_id returns 404.
  • /api/integrations/google/drive/items/upload
    • POST: uploads one local file into a folder of the caller's own Google Drive (resumable Drive upload, modules:api:write), records it in the Drive mirror immediately, and imports it into Dokumente. Multipart form: file, folder_id, optional account_type and title (last path segment is used as the Drive file name); max 100 MB. Returns the dokumente row plus drive_file_id and web_view_link. 404 when the folder is not in the caller's mirror, 422 for types Clapilot cannot import, 502 for Google errors (detail). Used by the Steuer-Manager to file uploads into a report's linked Drive folder.
  • /api/integrations/google/drive/items/import-folder
    • POST: imports the next batch of not-yet-imported, importable files anywhere below a folder of the caller's own Drive mirror (modules:api:write). Body: { folder_id, account_type?, limit? } (limit 1-25, default 10). Returns { processed, imported, skipped, failed, remaining, errors }; clients repeat until remaining is 0 (used by the Steuer-Manager folder source). Files that failed three times drop out of the queue; 404 when the folder is not in the caller's mirror.
  • /api/integrations/google/drive/items/import
    • POST: imports one file of the caller's own Drive mirror into Dokumente on demand (modules:api:write). Body: { file_id, account_type? }. Returns { status, document, error } where status is imported|updated|unchanged|duplicate; 404 when the file is not in the caller's mirror, 422 for folders, unsupported types, or files rejected after download, 502 for Google download failures (detail carries the upstream message).
  • /api/integrations/microsoft/oauth/start
    • POST: starts Microsoft OAuth. Accepts scope_presets / scopes as before plus optional redirect_path for same-origin app paths such as /?onboarding_step=1; when present, the OAuth callback redirects back to that path with microsoft_oauth result parameters instead of defaulting to /settings/app-verbindungen.
  • /api/integrations/microsoft/oauth
  • /api/integrations/microsoft/oauth/complete
  • /api/integrations/microsoft/oauth/status
    • GET: returns connection state, granted scopes, connected account info, and per-service enablement flags including Microsoft Mail and OneDrive
    • PATCH: updates one or more per-user Microsoft service toggles via service_settings
  • /api/integrations/x/oauth/start
  • /api/integrations/x/oauth
  • /api/integrations/x/oauth/complete
  • /api/integrations/x/oauth/status
    • GET: returns connection state, granted scopes, connected account info, and per-service enablement flags
    • PATCH: updates one or more per-user X service toggles via service_settings
  • /api/integrations/microsoft/calendar/sync
    • POST: imports enabled Microsoft calendar events into calendar_entries; accepts optional from, to, max_results, and calendar_id.
  • /api/integrations/microsoft/contacts
  • /api/integrations/microsoft/files
  • /api/integrations/microsoft/drive/sync
    • POST: imports enabled Microsoft OneDrive files into the virtual Microsoft 365 document folder; the sync walks nested OneDrive folders up to max_files and upserts matching dokumente rows.
  • /api/integrations/microsoft/todo/lists
    • GET: returns { lists: [{ id, display_name, wellknown_list_name, is_owner, is_shared }], links: [{ list_id, list_name, board_id, last_synced_at, last_error }] } for the signed-in user's Microsoft To Do lists and their board mappings. last_error is a stable error code (not_connected, disabled, scope_missing, graph_error) that clients translate, or null.
    • PUT: replaces the mapping set with { links: [{ list_id, list_name, board_id }] }; lists not included are unmapped (already mirrored tasks stay). board_id must be a shared board or the user's private board.
    • Errors return { error, code } with code not_connected, disabled (400), scope_missing (403, reconnect needed), or graph_error (502).
  • /api/integrations/microsoft/todo/sync
    • POST: one-way sync of all mapped To Do lists into aufgaben (initial import, then Graph delta query). Returns { lists: [{ list_id, list_name, created, updated, unlinked, error }], created, updated, unlinked, failed_lists }; a failing list is reported in its entry (error = one of the codes below) and does not abort the others. A Graph 401, or a 403 on every mapped list, is connection-wide and fails the whole request with scope_missing (403) so the client asks the user to reconnect. Same error codes as above.
  • /api/integrations/microsoft/oauth/status
    • PATCH additionally accepts service_settings.tasks_enabled; the status payload includes service_settings.tasks_enabled. The tasks scope preset (Tasks.Read) is part of the default Microsoft connect presets.

Heartbeat

  • /api/heartbeat/settings
    • GET: read the heartbeat config, job status, and open watchlist for ?scope=user (own config) or ?scope=teamchat (admin only); status includes lastRunAt, nextRunAt, lastDeliveryStatus (delivered / suppressed / error), and lastError
    • POST: save { scope, config } with config = { enabled, intervalMinutes, instructions, historyLimit, dndStart?, dndEnd?, sessionId? | roomId? }; the runtime reconciles the schedule within about a minute
  • /api/heartbeat/trigger
    • POST: run the heartbeat immediately with { scope } (teamchat scope requires admin); proxies to the native runtime POST /internal/heartbeat/trigger and returns { ok, delivered, suppressed, text, outputPreview, watchlistAdded, watchlistResolved, runId } without shifting the regular schedule
  • /api/heartbeat/watchlist
    • DELETE: remove one open watch item with ?scope=user|teamchat&id=<uuid> (teamchat scope requires admin); returns the remaining watchlist

Admin and app settings

  • /api/admin/users
    • GET: list all users plus merged user_profiles role/display-name/avatar data, users.last_login_at, and invitation state (invitation_pending, plus invitation_sent_at / invitation_expires_at of the newest active invitation link) for the admin settings page
    • POST { email, role?, display_name? }: invite a user. Creates the account with an unusable random password and invitation_pending = true, then emails a one-time link to /set-password?token=… (valid 7 days) through the instance system mailbox (agent email SMTP settings; Reply-To is the inviting admin). Returns { user, invitation } where invitation is { purpose, email, email_status: "sent" | "failed" | "not_configured", expires_at, link }; link is only returned when the email did not go out, so the admin can share it manually. No temporary password is ever generated or returned
    • PATCH: update a user's role, update profile/account fields with { action: "update-profile", userId, email, display_name, avatar_url } (changing the login email revokes outstanding links), set a password directly with { action: "set-password", userId, password } (also settles a pending invitation), or send an access link with { action: "resend-invitation" | "send-password-reset", userId } → { success, email, invitation }. The server derives the link purpose from the account state: pending users get a fresh invitation (7 days), active users a password reset link (24 hours) while their current password keeps working until they set a new one. Issuing a link revokes the user's previous links. The legacy { action: "reset-password" } is an alias of send-password-reset and no longer returns a temporary password
    • DELETE: remove a user after clearing legacy non-cascading references in mandanten, dokumente, aufgaben, and notizen; self-delete is rejected
  • /api/admin/backup/export
    • POST: admin-only export that generates a ZIP download containing database.sql from pg_dump, workspace/** from the full configured Clapilot workspace directory, and a small manifest.json
  • /api/admin/backup/import
    • POST: admin-only multipart import for a previously exported backup ZIP; requires backup file plus confirmation text, restores database.sql with psql --single-transaction, and replaces the configured workspace directory from workspace/**; older documents/** backups are still accepted and only replace the shared mandanten/ document tree
  • /api/admin/demo-data
    • GET: admin-only demo-seed status for the current admin user, including current seeded record counts, mailbox/document preflight, active scenario (steuerberaterkanzlei or rechtsanwaltskanzlei), scenario options, last run metadata, and the shipped demo storylines for the active scenario
    • POST: admin-only demo-seed execution; accepts { action, scenarioId } where scenarioId is one of steuerberaterkanzlei or rechtsanwaltskanzlei
    • the rechtsanwaltskanzlei scenario seeds 20 distinct synthetic legal demo documents across Kanzlei/Berufsrecht, Zivilrecht, Verkehrsrecht, Arbeitsrecht, Familienrecht, and M&A/Due-Diligence workflows; fixtures are first-party demo texts with fiktive parties, dates, file metadata, and workflow-ready deadlines/tasks
    • reset_and_seed performs a destructive demo reset for the current admin user's seeded Mandanten, Dokumente, Aufgaben, Kalender, Drafts, Automationen, mailbox cache rows, and tagged IMAP demo mails before rebuilding the selected scenario; seeded dates are generated relative to the reset time and kept in the active month where possible, so demo deadlines and recording flows do not age into stale fixed dates
    • seeded automations may include scenario-specific draftSubject and draftBodyText copy so demo replies can contain concrete legal/tax reasoning instead of generic acknowledgements
    • reset_emails, reset_tasks, and generate_activity always operate on the currently active seeded scenario, even if a different scenarioId is sent, so partial resets cannot accidentally mix two different Kanzlei demos
    • wipe_operational_data deletes all app-local Mandanten, Dokumente, Aufgaben, Kalender, local email cache rows, drafts, automations, document folders, and the physical files under the shared document storage across the instance while keeping users, roles, passwords, global settings, and external IMAP mailbox contents unchanged
  • /api/admin/agent-runtime/files
    • GET: list editable native runtime files (clapilotaicore.json, workspace markdown, native state markdown)
    • PUT: update the selected native runtime file; JSON writes are validated and normalized
  • /api/admin/agent-runtime/restart
    • POST: returns 410 because the packaged OpenClaw gateway restart path was removed; restart native services via Docker/deployment tooling instead
  • /api/admin/agent-runtime/terminal
    • GET: list builtin native runtime diagnostics only
    • POST: run builtin diagnostics like node-version, ls-state-dir, show-clapilotaicore-json, db-host-check, and agent-transport-check (app → runtime transport counters plus a live DNS + /health probe as text; same data as /api/agent-runtime/transport-status?probe=1)
  • /api/admin/anthropic/oauth/start
    • POST: deprecated for Anthropic setup-token auth and now returns an instructional error; Anthropic-Claude expects a finished claude setup-token value instead of a browser callback flow
  • /api/admin/anthropic/oauth/complete
    • POST: accepts a Claude setup-token (sk-ant-oat01-...), validates it, and stores it as the subscription secret for Anthropic-Claude
  • /api/admin/anthropic/oauth/clear
    • POST: admin-only removal of the stored Anthropic-Claude subscription secret/setup-token
  • /api/admin/openai-codex/oauth/start
    • POST: starts a short-lived Codex app-server chatgptDeviceCode login and returns login_id, verification_url, user_code, and expires_at; no localhost callback or pasted redirect URL is required
  • /api/admin/openai-codex/oauth/status
    • POST: accepts login_id plus provider_slug, returns pending while OpenAI authorization is outstanding, and on completion imports the Codex-managed credential, synchronizes the native Codex auth stores, and saves the encrypted provider secret
  • /api/admin/openai-codex/oauth/complete
    • POST: legacy compatibility endpoint for completing an already-started authorization-code flow from a callback URL/query; current web and Apple provider settings use device-code status polling instead
  • /api/admin/storage
    • GET: admin-only storage status of this instance. Returns disk (live statfs of the workspace volume: freeBytes, usedBytes, totalBytes, usedPercent, level of ok|warning|critical, underPressure, the effective thresholds) and report (the latest cleanup-worker attribution report from <state dir>/instance-storage/report.json with categories[] per source, retention[] rules, database size, and the last cleanup cycle; null until the worker has written one). Backs /admin/storage and the instance_storage_status agent tool
  • /api/admin/network-storage
    • GET: admin-only overview of SMB shares mounted into the workspace (see Network Storage). Returns mounts[] (id, folderName, shareUrl normalized to smb://…, username, domain, hasPassword, smbVersion, readOnly, enabled, configRevision, summary of mounted|partial|error|pending|disabled, statuses[] per service with service, state, errorCode, message, totalBytes, freeBytes, guardActive, mountedAt, checkedAt, stale, current), services[] (service, canMount, capabilityError, checkedAt, stale) and relocations[] (id, mountId, sourcePath, targetPath, state of queued|copying|switching|finalizing|done|failed, phase, bytesTotal, bytesCopied, localBackupPath, error, timestamps). The password is never returned
    • POST: create a share from folderName, shareUrl, optional username, password, domain, smbVersion (auto|3.1.1|3.0|2.1|2.0), readOnly, enabled. Returns 201 { mount }. Validation errors return 400/409 with localized error and code (folderRequired, folderInvalid, folderReserved, folderTaken, folderConflict, shareRequired, shareInvalidScheme, shareCredentialsInUrl, shareInvalidHost, shareInvalidPort, shareMissingShare, shareInvalidShare, shareInvalidPath, credentialInvalid, credentialTooLong, smbVersionInvalid)
  • /api/admin/network-storage/{id}
    • PATCH: update shareUrl, username, domain, password (non-empty replaces), clearPassword, smbVersion, readOnly, enabled, or send retry: true to force an immediate remount. The folder name is fixed. Every change bumps configRevision and notifies all services. Returns { mount }
    • DELETE: unmount and remove the share and its empty workspace folder in every service; NAS files are untouched. 409 hasRelocations while workspace folders were moved onto it
  • /api/admin/network-storage/relocations
    • GET: candidates[] (path, bytes from the storage report or null) of workspace folders that may be moved onto a share
    • POST: start moving sourcePath onto the share mountId. Returns 201 { id }; errors relocationInvalid, relocationNotAllowed, relocationReadOnly, relocationNotMounted (the share must be mounted in every service), relocationActive
  • /api/admin/network-storage/relocations/{id}/retry
    • POST: resume a failed move from its pre-checks (not switched yet) or its verification step (switched)
  • /api/admin/rag/status
    • GET: return RAG health, queue diagnostics, the resolved embedding provider/model currently used by native memory + document RAG, current document vision fallback config, and vision-capable model options derived from Provider & Modelle
    • POST: update the explicit document vision fallback toggle and selected vision model ref
  • /api/admin/rag/reindex
  • /api/app-settings
    • GET: returns global app settings for the current user role; responses include memory_dreaming_model as an exact provider/model ref or "" for automatic Dreaming-model selection, main_agent_tool_restrictions_enabled (default false) for the explicit main-agent opt-in, and main_agent_disabled_tool_names as the stored per-tool denylist. hosted_workspace is true in hosted workspaces (CLAPILOT_AUTH_MODE=portal), where the settings navigation shows team and platform management from the portal. Admins also receive email_auto_analysis_enabled, email_analysis_model, and email_auto_process_blocklist for the mail automation flow
    • POST: admin-only update of global app settings; accepts memory_dreaming_model as an exact provider/model ref or an empty string, returning 400 for malformed refs, plus main_agent_tool_restrictions_enabled and main_agent_disabled_tool_names to activate and configure exact main-agent tool restrictions. Unknown tool names are discarded against the canonical runtime catalog. It also accepts email_auto_analysis_enabled, email_analysis_model, and email_auto_process_blocklist to control automatic personal inbox analysis, the preferred workflow model, and the global sender/domain skip list for automatic mail processing
    • persists global app integrations including github_token, the optional github_pr_review_token, Google OAuth client credentials, Microsoft OAuth client credentials plus tenant hint, X OAuth client credentials, LinkedIn OAuth client credentials, plus legacy fallback audio provider secrets used outside native provider rows
    • GET: return persisted global app toggles including document-processing mode, native runtime base URL, chat transport mode, the global search-provider choice (default_search_provider), the SearXNG instance URL (searxng_search_base_url), the first-login onboarding toggle (user_onboarding_enabled), the Mandanten profile crawl toggle (mandant_profile_web_crawl_enabled), the LiteLLM base URL (litellm_api_base_url), and admin-only key presence flags such as has_brave_search_api_key, has_perplexity_api_key, has_litellm_api_key, has_linkedin_oauth_client_id, has_linkedin_oauth_client_secret, has_x_oauth_client_id, and has_x_oauth_client_secret
    • GET: admin responses still include legacy chat_tts_provider, chat_tts_model, chat_tts_voice, has_google_gemini_api_key, and has_elevenlabs_api_key fields for compatibility, but the active global TTS/STT/image defaults now live under /api/agent-runtime/config
    • POST: update persisted global app toggles and global provider keys used outside per-provider runtime config, including the X OAuth client id/secret used by the X app connection flow, the LinkedIn OAuth client id/secret used by the bundled Social Media module's LinkedIn platform, optional Gemini and ElevenLabs fallback API keys, the global search-provider choice, Brave Search / Perplexity API keys, and searxng_search_base_url used by native web-search/enrichment paths, the LiteLLM base URL/API key used by the LiteLLM usage inspector, the first-login onboarding toggle, the Mandanten profile crawl toggle, the admin-only Developer mode toggle, and the tab_layout_enabled toggle for the instance-wide Clapilot Tab Layout web shell (default on since migration 283; also returned by GET for all authenticated users so the app shell can pick the layout variant)
  • /api/app-connections/github
    • GET: admin-only list of named GitHub integrations with masked tokens and update timestamps (used by Website Canvas, Agent Orchestrator, and Issue Reporter target selection)
    • POST: admin-only manage action with { action: "create" | "update" | "delete", name, token?, previous_name? }
  • /api/app-connections/gitlab
    • GET: admin-only list of named GitLab integrations with instance URL, masked token presence, and update timestamps
    • POST: admin-only manage action with { action: "create" | "update" | "delete", name, token?, base_url?, previous_name? }; base_url supports GitLab.com and self-managed GitLab
  • /api/integrations/browser-use/settings
    • GET: admin-only Browser Use Cloud configuration status; returns { configured, profiles_provider } and never returns the saved key
    • POST: admin-only save or removal of the encrypted Browser Use API key via { api_key?, clear_api_key? } (accepted keys start with bu_), plus the active provider for new persistent browser profiles via { profiles_provider: "local" | "browser-use" } (default local)
  • /api/integrations/browser-use/profiles
    • GET: lists only the authenticated user's persistent browser-profile metadata including each profile's provider (local or browser-use), plus the instance-wide { configured, provider }; provider IDs, cookies, passwords, and storage state are never returned
    • POST: creates a dedicated profile from { name, consent: true } using the active provider; explicit consent is mandatory and audited. Local profiles store their Chromium user-data-dir under the shared workspace volume (.browser-profiles/<id>), so login state never leaves the instance; the directory is created before the row (row and profile_created audit event are written together), and when it cannot be created nothing is stored and the route answers 500 with a localized storage error
    • DELETE ?id=<uuid>: deletes an owned profile (remote profile for browser-use, on-disk data dir for local) and records the deletion in the audit trail
  • /api/integrations/browser-use/profiles/login
    • Cloud (browser-use) profiles only; local profiles sign in through /api/browser-profiles/login-sessions
    • POST { action: "start", profile_id }: starts a metered interactive remote browser canvas for an owned profile and returns its temporary liveUrl
    • POST { action: "confirm", profile_id, session_id }: stops the login session so Browser Use persists its state, marks the profile ready, and audits the confirmation
    • Agent discovery uses browser_profiles_list, which returns the same safe owned-profile metadata (including provider) before browser_use_run.profile_id is selected; browser_use_run rejects local profiles.
  • /api/browser-credentials
    • Saved website logins that agents fill into the caller's own agent browser (personal; every query is scoped to the authenticated user). Passwords are AES-256-GCM encrypted (enc:v1:) and are never returned by any route or agent tool.
    • GET → { credentials: [{ id, label, siteUrl, host, allowHttp, username, hasPassword, lastUsedAt, createdAt, updatedAt }] }
    • POST { siteUrl, username, password, label? } → 201 { credential }. siteUrl accepts a domain or login URL; it is normalized to host (lower case, leading www. removed). Only http(s) addresses with a dotted, non-IP host and no embedded credentials are accepted; allowHttp is true only for an explicit http:// address. Errors: 400 (invalidSite, usernameRequired, passwordRequired, limit — 200 per user), 409 duplicate (same host + username), each with a localized error and a code.
  • /api/browser-credentials/:id
    • PATCH { siteUrl?, username?, password?, label? } → { credential }; an omitted or empty password keeps the stored one. 404 notFound for ids of other users.
    • DELETE → { ok: true }
    • Create/update/delete and every agent fill or refused fill are recorded in agent_browser_credential_audit_events (host and outcome only, never the secret).
  • /api/browser-profiles/login-sessions
    • POST { profile_id }: starts an interactive in-instance sign-in session for an owned local profile. Launches a persistent-profile Chromium inside the web container and returns { sessionId }. One open session per profile, at most two per instance; sessions expire after 10 minutes idle / 30 minutes total. Audited as login_started
  • /api/browser-profiles/login-sessions/:id
    • POST { action: "complete" }: closes the browser so the profile state is flushed to disk, marks the profile ready, and audits login_confirmed
    • DELETE: cancels the session without marking the profile ready; audits login_cancelled
  • /api/browser-profiles/login-sessions/:id/stream
    • GET: owner-only Server-Sent-Events stream of the live browser view — CDP screencast JPEG frame events (latest-frame-wins, ~12 fps max), main-frame url changes, and a terminal closed event with reason: completed | cancelled | expired | error
  • /api/browser-profiles/login-sessions/:id/input
    • POST { events: [...] }: owner-only batched input forwarding into the live view (mousemove, mousedown, mouseup, wheel, keydown, keyup, and inserttext for clipboard/IME text). Events are validated by the shared page-input dispatcher (unknown types and malformed fields dropped, coordinates/deltas clamped, keys ≤ 32 chars, text ≤ 4,096 chars, at most 120 events per batch); single characters outside printable ASCII in keydown (umlauts, ß, €, emoji) are inserted as text, and Meta maps to the server platform's primary shortcut modifier. Individual event failures are ignored so a batch cannot kill the session
  • /api/browser-profiles/login-sessions/:id/navigate
    • POST { url }: owner-only navigation of the live view; only http(s) and non-private hosts are allowed (localhost, RFC1918 ranges, .local/.internal, and single-label compose hostnames are rejected)
  • /api/browser-profiles/agent-session
    • GET: the caller's own agent browser session (driven by the browser_* tools) as a cheap in-memory lookup → { active: false } or { active: true, sessionId, profileName, url, startedAt, controller, controlSince }. sessionId is a stable random id per session (used by the web app to remember a dismissed live window); controller is agent or user (the user took the session over), controlSince the ISO time of the last control change (null until the first take-over); 401 without a session cookie. Polled by the floating live agent browser window (see Browser Profiles)
  • /api/browser-profiles/agent-session/stream
    • GET: owner-only Server-Sent-Events stream of the caller's agent browser session. Events: status { active: true, sessionId, profileName, url, startedAt, controller, controlSince } (sent first, and again whenever the controller changes or a popup the user opened becomes the driven page), frame { data, width, height } (CDP screencast JPEG, latest-frame-wins, ~12 fps max; the latest frame is replayed to a new subscriber), url { url } on main-frame navigation, and a terminal closed { reason: closed | expired | replaced | handoff | not_found } after which the stream ends (not_found immediately when the user has no session). : ping comments every 15 s. The screencast starts with the first subscriber and stops when the last one disconnects; watching never extends the session's idle timeout. 401 without a session cookie
  • /api/browser-profiles/agent-session/control
    • POST { sessionId, action: "take" | "release" }: take over or hand back the caller's own agent browser session. sessionId is optional but, when given, must match the current session. Returns { ok: true, ...status } (the GET /api/browser-profiles/agent-session shape with the new controller). While controller is user, the agent's page-changing browser tools are refused (browser_snapshot/browser_read stay available), user input counts as session activity (5 min idle timeout from the last input), and the 20 min max age is paused. A take-over is audited as profile_used with via: "agent_browser_takeover". release lets go of any keys/mouse buttons still pressed. 400 for an unknown action, 401 without a session cookie, 404 when the user has no session or sessionId does not match
  • /api/browser-profiles/agent-session/input
    • POST { sessionId, events: [...] }: batched pointer/keyboard input into the caller's agent browser session, same event format, validation, and limits as /api/browser-profiles/login-sessions/:id/input. Only accepted while the caller controls the session: 409 otherwise, 404 without a (matching) session, 401 without a session cookie
  • /api/browser-profiles/agent-session/navigate
    • POST { sessionId, url }: URL bar of the live window while the caller controls the session; http(s) only, private/compose-internal hosts rejected like the sign-in live view. 400 for a missing or rejected URL, 404 without a (matching) session, 409 without control, 401 without a session cookie
  • /api/developer/e2e
    • GET: admin-only endpoint that requires app_settings.developer_mode_enabled=true; returns the in-instance Developer E2E suites and model options used by Settings -> Developer
    • POST: admin-only execution endpoint for one suite with { suiteId, model?, keepArtifacts? }; runs against the current Clapilot instance and returns a structured result with suite status, runtime session/run identifiers, model, cleanup state, per-check results, output preview, and runtime metadata
    • webchat-history-replay-context seeds stale completed webchat agent_runs plus persisted synthetic transcript wrappers, executes one current live webchat turn through ClapilotAICore, and verifies the current run stores only the latest user message, caps stored-history replay, reports dropped synthetic history turns, and avoids stale marker output
    • canvas-create-edit-file sends the two German chat requests for creating a tax-declaration Canvas file and then editing it to Max MusterFrau with a 4000 EUR refund; it verifies the created HTML file exists, contains the global Canvas style colors, and is edited in place with the requested data
    • by default suites delete their seeded agent_runs, agent_events, agent_context_messages, agent_session_state, and suite-owned generated Canvas files; keepArtifacts=true leaves them behind for diagnostics
  • Native runtime /internal/runs
    • accepts optional idempotencyKey / messageId for per-session turn deduplication; webchat sends the persisted user message row id
    • serializes turns per sessionKey; queued requests stay pending and emit a lifecycle event with phase="queued" before the normal start/end stream completes
  • /api/litellm/usage
    • GET: admin-only proxy to the configured LiteLLM /user/daily/activity endpoint; returns normalized totals plus day-by-day model/provider/api-key breakdowns for Settings -> ClapilotAICore -> LiteLLM
  • /api/litellm/logs
    • GET: admin-only proxy to the configured LiteLLM /spend/logs endpoint with summarize=false; returns normalized individual spend-log rows plus raw metadata payloads for drill-down inspection
  • /api/subscription-usage
    • GET: admin-only live subscription/quota snapshot for Codex, Claude Code, Grok, Ollama, and Cursor, with 60-second server caching and normalized progress-window data for the Settings -> ClapilotAICore -> Subscription Usage page
    • include_connected=1 returns { checkedAt, hubMode, reportIntervalMs, staleAfterMs, instances } for the compact Settings -> Hub -> Subscription Usage page. instances[0] is the API Hub itself; on a local Hub, later entries are the latest pushed snapshots from connected instances. Loading this aggregate does not contact remote instances or their providers
    • each windows[] entry includes the stable upstream key, a normalized English label, usedPercent, utilization, resetAt, and limitWindowSeconds. Codex labels are derived from limitWindowSeconds instead of assuming primary_window is five hours or secondary_window is seven days; absent upstream windows are omitted. Claude recognizes the Fable 5 weekly bucket as seven_day_overage_included and labels it 7d Fable 5
    • Claude Code live usage is served from Clapilot-owned credentials only. Full Claude OAuth credentials use Anthropic's OAuth endpoints (/api/oauth/usage for windows, /api/oauth/profile for the plan) with the Claude OAuth beta header and a Claude Code user agent. Claude web-session credentials use Claude's claude.ai/api/organizations/.../usage path with the stored sessionKey plus full browser cookie header when Cloudflare requires it. The credential chain is: CLAPILOT_CLAUDE_OAUTH_TOKEN, the Claude CLI login home $CLAUDE_CLI_HOME/.claude/.credentials.json written by the settings Claude Auth flow (source claude_cli_home), Anthropic provider rows with auth_mode=oauth_token, app_settings.anthropic_oauth_token, and ~/.claude/.credentials.json
    • full Claude Code OAuth credentials (scopes include user:profile) are refreshed automatically against platform.claude.com/v1/oauth/token when expired, and rotated tokens are written back to the credentials file shared with the agent runtime CLI
    • bare sk-ant-oat01-... setup-token provider rows first run the Docker-local Claude CLI usage probe with the same CLAUDE_CODE_OAUTH_TOKEN bridge environment used for inference. If the CLI does not return subscription windows, the endpoint checks Anthropic's OAuth usage endpoint only as diagnostics; Anthropic rejects setup-tokens there with 403 user:profile
    • credential candidates holding the same token are deduplicated per snapshot, and scope-rejected tokens are remembered in-process for 6 hours so auto-refreshing panels do not repeatedly hit (and rate-limit) the upstream usage endpoint
    • Grok usage reuses an xAI provider configured with auth_mode=oauth_token, refreshes that OAuth credential through the existing xAI flow when needed, and reads the current subscription credit percentage/reset from grok.com's GrokBuildBilling/GetGrokCreditsConfig gRPC-web endpoint
    • Ollama Cloud usage reads the plan, account, session/hourly percentage, weekly percentage, and reset timestamps from https://ollama.com/settings. Ollama API keys authenticate inference and return per-request metrics, but do not expose these account plan windows. The required browser Cookie header is stored in the encrypted Ollama provider secret bundle and remains eligible when that provider has no routed models; CLAPILOT_OLLAMA_COOKIE is an optional deployment override
    • Cursor usage reuses the encrypted User API key from the configured cursor provider. Clapilot exchanges it for a short-lived Cursor account token, reads the current billing-cycle usage, and returns the included-usage percentage, remaining balance, monthly limit, consumed amount, and reset date. Neither the API key nor the short-lived token is included in the snapshot or Hub report
  • /api/admin/developer/api-keys
    • GET: admin-only list of instance API-key metadata plus subscription_usage_url, notifications_url, memory_url, tools_url, issue_reports_url, and the current Issue Reporter app/repository catalog, built from the configured app_settings.public_base_url; requires app_settings.developer_mode_enabled=true and never returns key hashes or plaintext secrets
    • POST: admin-only create path with { name, scopes, expires_at?, allowed_repositories? }; accepts subscription_usage:read, notifications:read, memory:read, memory:write, inference:execute, and the high-privilege tools:execute scope in any non-empty combination. issue_reports:write must be the key's only scope and requires at least one full repository from the current Issue Reporter catalog. Public-client issue keys use the clp_public_ prefix. The plaintext key is returned once, while only its SHA-256 hash and display prefix are persisted. Notification, memory, and tool execution are bound to the admin user who created the key
  • /api/admin/developer/api-keys/[keyId]
    • DELETE: admin-only immediate revocation for an active instance API key; requires Developer mode
  • /api/v1/subscription-usage
    • GET: versioned device/API-client endpoint returning the same 60-second-cached normalized five-provider snapshot as /api/subscription-usage
    • by default the response contains only the subscription accounts connected to the instance serving the API. Add include_connected=1 (or include_connected=true) on a local Hub to receive the aggregate { checkedAt, hubMode, reportIntervalMs, staleAfterMs, instances }, including the Hub followed by every connected instance that has reported a valid snapshot
    • aggregate entries contain { id, host, sourceInstanceId, instanceUrl, isLocal, checkedAt, reportedAt, receivedAt, stale, snapshot }. A report becomes stale after 45 minutes; the stored snapshot remains visible so clients can distinguish missing data from an outdated report
    • clients must use each window's label and limitWindowSeconds rather than assigning durations by array position or by Codex primary_window / secondary_window; upstream providers can temporarily remove or reorder quota windows
    • authenticate with Authorization: Bearer clp_live_... (preferred) or X-API-Key; the key must be active, unexpired, and grant subscription_usage:read
    • returns stable JSON auth errors with 401 for missing/invalid/expired/revoked keys and 403 for a missing scope or disabled Developer mode; successful requests update the key's last_used_at
    • examples: curl -H 'Authorization: Bearer clp_live_...' https://your-instance.example/api/v1/subscription-usage and, on a Hub, curl -H 'Authorization: Bearer clp_live_...' 'https://your-hub.example/api/v1/subscription-usage?include_connected=1'
  • /api/v1/notifications
    • GET: read-only polling endpoint for the API-key creator's durable message notification inbox. It returns personal assistant replies and Team Chat messages with the same clapilot_type, title/body, message_id, session_id / room_id, sender_name, and author_kind routing metadata used by Apple push notifications
    • authenticate with an active clp_live_... key granting notifications:read; Developer mode remains the instance-wide kill switch. A key without an associated creating user is rejected with 403 user_scope_required
    • accepts limit=1..100 (default 20) and an opaque after cursor. Responses contain { notifications, has_more, next_cursor, poll_after_ms }; clients should retain next_cursor, pass it as after on the next poll, and continue immediately while has_more=true, otherwise waiting at least poll_after_ms
    • the first request without after returns the latest page in chronological order. Polling is non-destructive and does not change chat read state; in this contract, "unread" means notification events newer than the consumer's saved cursor
    • example: curl -H 'Authorization: Bearer clp_live_...' 'https://your-instance.example/api/v1/notifications?limit=20', followed by ...?after=<next_cursor>
  • /api/v1/memory
    • GET: creator-bound semantic search over approved, active native memory visible to the user who created the API key. Requires memory:read, a non-empty query of at most 1,000 characters, and optional limit=1..20 (default 6). Results contain { id, title, content, score, memory_scope, visibility_scope, source_type, retrieval_mode }
    • POST: submit explicit durable memory with a memory:write key and JSON { content, title?, visibility_scope? }. content is required and limited to 20,000 characters; title is limited to 200 characters; visibility_scope may be private or team. The native storeManualMemory path applies content safety, audience derivation, user sharing preferences, content-hash deduplication, assertion status, embeddings, and the Learning review path rather than inserting raw memory rows
    • safe, non-conflicting facts may return 201 with an approved memory id. Deduplicated submissions return 200. Content requiring review returns 202 with id=null, assertion_status="candidate", and needs_review=true; it does not become searchable until approved. Stable response fields are { memory: { id, assertion_id, assertion_status, needs_review, deduplicated, title, memory_scope, visibility_scope } }
    • Developer mode, key expiration/revocation, and the creating user remain enforced on every request. A memory:write key does not imply memory:read, and a memory:read key does not imply memory:write
    • rate limits are enforced atomically in PostgreSQL per API key: reads allow 120 requests per 10 minutes and 5,000 per day; writes allow 60 requests per 10 minutes and 500 per day. 429 rate_limited responses include Retry-After
    • examples: curl -H 'Authorization: Bearer clp_live_...' 'https://your-instance.example/api/v1/memory?query=project%20preferences&limit=6' and curl -X POST -H 'Authorization: Bearer clp_live_...' -H 'Content-Type: application/json' --data '{"title":"Project preference","content":"Prefer live runtime evidence.","visibility_scope":"private"}' 'https://your-instance.example/api/v1/memory'
  • /api/v1/memory/[memoryId]
    • GET: retrieve one approved, active memory by UUID with memory:read. The runtime applies the same creator-bound visibility check as search and returns 404 memory_not_found when the entry does not exist or is not visible to that key
    • successful responses contain { memory: { id, title, content, memory_scope, visibility_scope, source_type, line_start, line_end, total_lines } }; internal metadata and source paths are not exposed
    • exact reads share the memory:read per-key rate-limit buckets with semantic search
  • /api/v1/tools/catalog
    • GET: returns the native runtime's curated coding_core tool profile as OpenAI function-tool definitions shaped as { profile, tools: [{ type: "function", function: { name, description, parameters } }] }. The catalog includes creator-bound memory, context, Knowledge, Learning, web-search, session/status, and scoped task-comment tools; it does not expose the broad business-action catalog
    • GET ?profile=personal_read: the coding core plus read-only personal/workspace tools that run as the key creator — emails_list_messages, emails_get_message, emails_list_drafts, emails_get_draft, calendar_list_events, calendar_get_event, aufgaben_list_boards, aufgaben_list_statuses, aufgaben_list_tasks, aufgaben_get_task, notizen_list_folders, notizen_list_notes, notizen_get_note, documents_list, documents_list_folders, documents_get, wiki_get_page. No mutation, send, delete, tool_execute, or exec_command entries are added. The profile changes only what a client is told about: tools:execute already executes any catalog tool as the creator, so personal_read grants no additional privilege. clapilot-code 0.4.2+ requests this profile
    • authenticate exactly like /api/v1/tools/execute with an active private clp_live_... key granting tools:execute. Developer mode remains the instance-wide gate. Catalog reads do not consume the tool-execution rate limit
    • example: curl -H 'Authorization: Bearer clp_live_...' 'https://your-instance.example/api/v1/tools/catalog'
  • /api/v1/tools/execute
    • POST: creator-bound remote execution for the same agent-tool catalog exposed by clapilot-cli. Authenticate with an active private clp_live_... key granting tools:execute and send { tool_name, arguments?, client_context?, ui_language? }
    • the server derives userId, sessionKey, and originSessionKey exclusively from the verified key. Client-supplied identity, service-principal, channel, run, and session fields are ignored, preventing a key from changing its creator-bound scope
    • calls reuse the native tool-proxy dispatcher, module availability checks, main-agent restrictions, approval workflows, UI mutation publication, and error contract. Tool-level failures remain HTTP 200 with top-level ok=false; authentication, malformed input, payload limits, and rate limits use normal 4xx responses
    • tools:execute is intentionally high privilege and includes read operations, mutations, outbound-capable tools, and exec_command. Grant it only to trusted server-side clients or coding agents, store it outside source control, set an expiration, and revoke it when no longer needed
    • request bodies are limited to 512 KiB. Rate limits are atomic per key: 300 calls per 10 minutes and 10,000 per day; 429 rate_limited includes Retry-After
    • example: curl -X POST -H 'Authorization: Bearer clp_live_...' -H 'Content-Type: application/json' --data '{"tool_name":"documents_list","arguments":{"limit":5}}' 'https://your-instance.example/api/v1/tools/execute'
  • /api/v1/inference/models
    • GET: OpenAI-compatible model list for stateless instance inference. Authenticate with Authorization: Bearer clp_live_...; the active private key must grant inference:execute, and Developer mode must be enabled
    • returns { "object": "list", "data": [{ "id": "provider-slug/model-id", "object": "model", "owned_by": "Provider label", "clapilot": { "tools": true } }] }. clapilot.tools is a non-standard capability flag indicating whether that model's resolved transport forwards client-supplied tool definitions. IDs are the same configured model references used by ClapilotAICore and include only models the inference provider layer can route without the server-side session/agent loop
    • example: curl -H 'Authorization: Bearer clp_live_...' 'https://your-instance.example/api/v1/inference/models'
  • /api/v1/inference/chat/completions
    • POST: OpenAI-compatible, stateless provider passthrough for remote CLIs and coding clients. Authenticate with an active private clp_live_... key granting inference:execute; provider credentials remain inside the Clapilot instance and are never returned to the client

    • accepts { model, messages, tools?, tool_choice?, temperature?, max_tokens?, max_completion_tokens?, stream?, stop?, response_format? }. model is one of the IDs returned by /api/v1/inference/models; messages uses the OpenAI chat-completions format. When both token-limit fields are present, max_completion_tokens takes precedence. Unknown request properties are ignored. n > 1 returns 400 unsupported_n; the legacy functions property returns 400 legacy_functions_not_supported and clients must use tools

    • non-streaming responses use the OpenAI chat.completion shape: { id, object, created, model, choices: [{ index, message: { role, content, tool_calls? }, finish_reason }], usage? }. Provider/auth/quota failures use { error: { message, type, code } } with an appropriate 4xx/5xx status and never include provider credentials, provider base URLs, or the internal agent secret. Supplying a non-empty tools array for a model whose clapilot.tools capability is false returns 400 with type: "invalid_request_error" and code: "tools_unsupported_for_model"; the request is rejected before any provider call rather than silently dropping tool definitions

    • stream=true returns text/event-stream with OpenAI chat.completion.chunk data records and a final data: [DONE]. Text deltas are forwarded as they arrive on streaming-capable provider transports. If a provider buffers tool calls, the completed tool_calls delta is emitted as one chunk before the terminal finish-reason chunk

    • this endpoint is deliberately not the Clapilot agent loop: it does not create agent runs or sessions, load Clapilot memory, or execute tools. Client-supplied tool definitions are forwarded to the selected model and any returned tool_calls must be executed by the client, which may then send tool results in a subsequent stateless request

    • request bodies are limited to 1 MiB. The model-list and completion endpoints share atomic per-key inference:execute limits of 300 requests per 10 minutes and 10,000 per day; 429 rate_limit_exceeded includes Retry-After

    • non-streaming example:

      curl -X POST \
        -H 'Authorization: Bearer clp_live_...' \
        -H 'Content-Type: application/json' \
        --data '{"model":"provider-slug/model-id","messages":[{"role":"user","content":"Reply with one short sentence."}]}' \
        'https://your-instance.example/api/v1/inference/chat/completions'
      
    • streaming example: add "stream":true to the JSON body and use curl -N so chunks are displayed without client-side buffering

  • /api/v1/issue-reports
    • POST: public-client issue intake for JSON requests authenticated with an active clp_public_... key granting the isolated issue_reports:write scope. app and title are required. The app is resolved to its full repository and must be present in the key's immutable allowed_repositories; checking only the repository basename is not sufficient
    • accepted optional fields are details, reporter_email, platform, route_path, route_url, context, installation_id, app_version, build_number, os_version, device_model, locale, and image_attachments. Attachments are base64-encoded PNG, JPEG, HEIC, HEIF, or WebP images, limited to three images, 5 MB each and 10 MB combined; the complete streamed JSON body is capped at 15 MB
    • accepted reports always enter hub_reported_issues with status open. They never directly create GitHub issues or Agent Orchestrator tasks; an admin must review and approve them in the Hub Issue Reporter queue
    • rate limits are enforced atomically in PostgreSQL per API key and per hashed client IP (10-minute and daily buckets). 429 responses include Retry-After. Send a stable unique Idempotency-Key for retries; repeating it with the same key returns the original report with created=false
    • the mobile key is a public identifier and abuse-limiting credential, not a confidential client secret. Prefer a B2C backend or App Attest/DeviceCheck exchange that mints short-lived report credentials; repository scoping, moderation, rate limits, expiration, and revocation remain defense in depth
  • /api/email-processing-blocklist
    • POST: admin-only helper endpoint used by the /emails row/detail action menus; extracts the sender address from the selected message and upserts it into app_settings.email_auto_process_blocklist, preserving optional reason metadata such as manual or auto-classified:marketing
  • /api/agent-runtime/assistant-message
    • POST: internal runtime-only endpoint guarded by x-clapilot-agent-secret; persists an assistant-origin automation/system message into the user’s main personal chat session, Teamchat #general, a concrete Teamchat roomId, or an approved external channel depending on the provided target payload. Team Chat deliveries import workspace-local video references as durable attachments before persisting; successful responses include message_id, attachmentCount, and attachments[] (attachmentId, index, type, name, mimeType, size, url). When a referenced video cannot be attached and the message is not a failure notice (messageMeta.ok === false), the request fails with 500 and nothing is persisted, so the runtime job fails visibly instead of announcing a file the room cannot open. Automation deliveries (messageMeta.automation_result === true with run_id) are idempotent per run and target: an existing message with the same automation_delivery_key (or the same run_id without a key) returns { ok: true, duplicate: true, message_id }. The optional originSessionKey (the run's session key; a :subscription-bridge:<kind> suffix is ignored) additionally lets a successful Team Chat delivery recognise an explicit agent post from the same run that carries no run id: a chat_group_messages row in the target room with message_meta.agent_explicit_team_chat_post = true, the same message_meta.origin_session_key, created after the run's started_at, and not stamped with a different run_id is returned as duplicate: true with matched_by: "origin_session" and no second message is stored
  • /api/agent-runtime/channel-mirror
    • POST: internal runtime-only endpoint guarded by x-clapilot-agent-secret; mirrors one external Telegram/WhatsApp/Slack/Signal/iMessage/instance-bridge group message into an explicitly mapped Team Chat room. Participant requests use { kind: "participant", channelType, roomId, text, sender: { key?, name?, username? }, eventId?, attachments? }; agent requests use { kind: "agent", channelType, roomId, text, agentName?, attachments?, uiActions?, canonicalSource? }. canonicalSource: true persists an agent reply without channelOrigin, waits for the mapped reverse delivery, and returns { ok: true, messageId, roomId, forwarded }; ordinary display copies retain the echo-loop origin marker. Card-only agent requests are valid when uiActions contains a renderable card. The endpoint rejects empty or unknown rooms instead of falling back to Teamchat #general and does not append agent replies to runtime session context.
  • /api/agent-runtime/channel-audio-transcription
    • POST: internal runtime-only endpoint guarded by x-clapilot-agent-secret; accepts { filePath, mimeType? } for an audio file already persisted inside the shared workspace, transcribes it through the configured STT runtime, and returns { ok: true, transcript, provider, providerType, model }. It is used by native channel ingestion before an agent run and never returns provider credentials.
  • /api/agent-runtime/request-logs
    • GET: admin-only list of persisted native model request logs with provider, model, duration, status, derived token counts, estimated prompt-layer attribution (promptLayerTokens), and raw usage/metadata payloads for the ClapilotAICore Logs inspector. requestKind is conversation, embedding, or image; image rows are written by the app's generated-image service with endpoint = image.generate | image.edit, the effective provider, metadata.base_url, and no token counters. Each log entry carries costUsd (number or null) and costSource (provider_reported | provider_override | price_table | null); subscription-backed rows mark metadata.costBasis = "api_equivalent". stats includes totalCostUsd (sum over matching priced requests, null when none is priced) and pricedRequests (count of rows with a cost) next to the token and cache totals.
  • /api/agent-runtime/learning
    • GET: admin-only list of learning objects, recent learning audit events, and grouped stats for the ClapilotAICore Learning inspector and exception-review queue
    • POST: admin-only create path for controlled durable facts, procedure/skill proposals, or hot memory snapshots; this records an initial learning audit event and creates pending approval state when required
  • /api/agent-runtime/learning/[id]
    • GET: admin-only detail view for one learning object with linked approval decisions and audit events
  • /api/agent-runtime/learning/[id]/decision
    • POST: admin-only approval ledger path for approved, rejected, changes_requested, revoked, or auto_approved_by_policy decisions; it updates the object's lifecycle state, appends an audit event, and backs both versioned system-policy activation and Learning settings exception-review actions
  • /api/build-info
    • GET: return runtime build metadata for both the web app container (clapilot) and the native agent service (clapilotAgent) so settings pages can detect version drift between the two services
  • /api/issue-reporter
    • POST: create an issue report using the configured Issue Reporter target (github, task_board, local_hub, or remote_hub) and attach the current page context, build info, and active chat transcript. JSON and multipart requests may send app as a repository basename without its owner prefix, for example app=clapilot-website; the backend resolves the full repository and mapped Task Board from the Agent Orchestrator repository matrix. Missing app remains backward-compatible and routes as clapilot. Unknown or ambiguous basenames are rejected. The request also accepts optional platform (web_ios_mac, web, ios, mac, or general) and multipart images[]. Created GitHub issues and Task Board tasks use the reporter summary as their title and record the Issue Reporter source, app, and repository in runtime context. The GitHub target uses the named Issue Reporter GitHub integration selected in Settings -> Issue Reporter; Hub approvals retain the submitted app mapping.
  • /api/issue-reporter/apps
    • GET: admin-only app catalog for the Issue Reporter selector. Local Hub instances return their own Agent Orchestrator repository-to-board mappings; remote-Hub spokes fetch the catalog from the configured Hub through the signed /api/hub/issues/apps contract and fall back to local mappings when the remote endpoint is unavailable.
  • /api/hub/status
    • GET: admin-only hub-mode status for the current normal Clapilot instance
  • /api/hub/validate
    • POST: signed hub handshake endpoint; when the sender includes instance_url, the local hub now auto-discovers or refreshes that instance in the monitored health list
  • /api/hub/connection-test
    • POST: admin-only connectivity test against the configured hub target (local or remote mode); validates URL/secret before saving hub settings. Remote mode also verifies the stored per-instance telemetry credential through /api/hub/telemetry/enroll, using the background sender's configured identity. A missing credential, rejected credential, or failed telemetry probe returns ok: false with an actionable error; a successful shared-secret handshake alone is insufficient. The test never issues credentials or uploads events.
  • /api/hub/channel-bridge/rooms
    • POST: HMAC-signed peer-room discovery for instance channel bridges; verifies the fleet hub signature (x-clapilot-instance-id, x-clapilot-ts, x-clapilot-signature) on the raw body, accepts {}, and returns this instance's mappable team-chat rooms as { rooms: [{ id, name, kind }] }. Available on every instance, not only in hub mode, so both bridge sides can serve it
  • /api/hub/channel-bridge/register
    • POST: HMAC-signed peer-side bridge registration; accepts { action: "upsert" | "remove", bridgeId, peerInstanceId, peerInstanceLabel, peerBaseUrl, peerRoomId, peerRoomLabel, localRoomId, localRoomLabel? }. upsert validates localRoomId against the mappable-room list, creates or updates the local approved instance_bridge channel-approval row with role peer, and enables the instance_bridge channel config; remove deletes the local approval row by metadata.bridge_id
  • /api/hub/issues/report
    • POST: signed inbound issue intake used when another Clapilot instance reports into this instance running in hub mode; also auto-discovers or refreshes the sending instance in the monitored health list when instance_url is present
  • /api/hub/issues/apps
    • POST: signed app-catalog endpoint used by remote-Hub spokes to render the same repository selector as the local Hub without exposing the catalog publicly
  • /api/hub/issues
    • GET: admin-only list of issue reports received by this hub-mode instance; accepts optional repository-basename app and status filtering. Supported status values are open, approved, denied (including legacy dismissed rows), github_failed, github_queued, task_failed, and resolved. The response also returns the configured app/repository/Task Board options for the Issue Reporter selector.
  • /api/hub/issues/[id]
    • GET: admin-only detail payload for one inbound issue report, including transcript and attachment URLs
    • PATCH: admin-only review action for one inbound issue report; { "action": "approve" } creates either a GitHub issue or an agent task depending on the hub instance's Settings -> Issue Reporter target and stores the resulting GitHub metadata or Aufgabe id, while { "action": "deny", "reason"?: string } marks the report as denied without forwarding and stores the optional reason in review_reason. Retrying a github_failed or task_failed report is another approve. The same review logic backs the admin-only agent tools issue_reporter_approve / issue_reporter_reject (src/lib/hub-issue-review.ts); every review decision is appended to hub_reported_issue_review_events with via = board or agent. The detail payload now also returns review_reason
  • /api/hub/health/instances
    • GET: admin-only list of monitored Clapilot tenants for this hub-mode instance, including manually added and auto-discovered rows plus discovery metadata such as source_instance_id, instance_url, discovery_source, and last_seen_at
    • POST: admin-only add a monitored tenant with optional admin credentials for login verification
  • /api/hub/health/instances/[id]
    • PATCH: admin-only update one monitored tenant
    • DELETE: admin-only remove one monitored tenant
  • /api/hub/health/instances/check
    • POST: admin-only run health checks for one or all monitored tenants
  • /api/hub/monitoring/alert-room
    • GET: admin-only current target channel for system monitoring alerts (hub customer monitoring + instance storage monitor). Returns room_id (configured value, empty = main Team Chat), fallback_room_id (clapilot-members), the effective delivery room (effective_room_id, effective_fallback, effective_fallback_reason = missing|deleted|archived|null), and rooms (non-deleted public/private Team Chat channels with id, name, kind, is_archived). Available on every instance, not only in hub mode.
    • PUT: admin-only update with { "room_id": "<chat_rooms.id>" }; an empty value or the main room resets to the default. Deleted, archived, or non-channel rooms are rejected with a localized 400. Persists app_settings.monitoring_alert_room_id and returns the same payload as GET. Intentionally not exposed as an agent tool.
  • /api/hub/fleet/instances
    • GET: admin-only Fleet instance inventory in local Hub mode; encrypted environment content is removed from responses
    • POST: creates a new Fleet instance from name, optional machineId, sipEnabled, whitelisted string overrides, and optional provisioningSettings.mainAgentToolRestrictionsEnabled (default true). Provisioning settings are persisted separately from raw environment overrides. The generated instance environment seeds the restriction flag exactly once, so later changes inside the spawned instance are not overwritten on restart
  • /api/local-db

Module platform

Module storage outside reads

Absolute read, list and stat paths require both an administrator session and enabled Developer mode in addition to the module manifest permission. Relative read targets resolve symlinks and remain beneath the actual module root. Ordinary workspace browsing continues through the File Explorer API and its existing workspace/state access policy.

  • /api/module-store/local
    • GET: signed-in local module inventory for /modules; returns all, effective, redacted paths for non-admins, and an installed boolean on every entry. Workspace/managed entries are always installed; bundled entries reflect the instance-wide DB policy and overrides, and uninstalled bundled entries remain in all but are excluded from effective. Manifest metadata includes icon, categories (store category keys, multiple per module), and hiddenInMenu; each entry also carries iconFile when the module ships an icon image (icon.png/icon.svg/icon.webp/icon.jpg) in its root, served via /api/modules/[slug]/assets/<iconFile>
  • /api/module-store/local/[slug]
    • GET: return only one effective module manifest for the module runtime page, with the same developer/admin visibility checks, localization, and non-admin path redaction as the full inventory. The module page uses this endpoint so renderer selection does not wait for the complete store payload.
  • /api/module-store/catalog
  • /api/module-store/publish
  • /api/module-store/install
  • /api/module-store/install-bundled
    • POST: admin-only install of a non-fixed bundled module. Runs unapplied bundled SQL migrations, upserts module_installs.installed = true, and synchronizes the legacy disabled file; it no longer copies bundled source into the workspace
  • /api/module-store/deactivate-bundled
    • POST: admin-only uninstall of a non-fixed bundled module. Upserts module_installs.installed = false, synchronizes the legacy disabled file, and removes a marked workspace clone left by the former copy-based install flow
  • /api/module-store/delete
  • /api/module-store/set-icon
    • POST: retired; authenticated admins receive 410 Gone with a localized error. Never modifies module.json; module icons are supplied by the module.
  • /api/module-store/set-menu-visibility
    • POST: admin-only update of module.json.hiddenInMenu; keeps the module active while removing or restoring its automatic sidebar menu entry
  • /api/modules/scaffold/new
  • /api/modules/[slug]/assets/[...assetPath]
  • /api/modules/[slug]/api/[...endpointPath]
  • /api/modules/[slug]/storage
    • POST: write, mkdir and delete accept only module-relative paths under data/ and reject symlink components. Policy failures return HTTP 400 with { "code": "dataOnly"|"symlink"|"codeTarget", "error": "localized message" }; error follows the request UI language (DE/EN/IT). Relative reads retain module-root containment; absolute reads retain administrator/Developer-mode and manifest gates.
    • All module runtime endpoints resolve only installed/effective modules and return 404 with {"error":"module_not_installed"} when the slug is unavailable

Appointment booking module API highlights:

  • /api/appointments/overview
    • GET: signed-in overview for the Termine module with settings, appointment types, availability windows, and upcoming appointments for an optional from/to date range
  • /api/appointments/settings
    • GET, PATCH: signed-in booking settings including timezone, slot step, minimum notice, booking horizon, public embed copy/enabled state, and optional internal_notification_email ([email protected] is the initial default only on Clapilot-managed hosts; an explicitly saved empty string or null disables internal notifications)
  • /api/appointments/types
    • GET, POST: signed-in appointment type list and upsert for fields such as name, duration_minutes, optional price_cents / price_currency, buffers, color, and active state
  • /api/appointments/availability
    • GET, POST, DELETE /api/appointments/availability/[id]: signed-in weekly bookable day/time windows, optionally scoped to one appointment type
  • /api/appointments/slots
    • GET: signed-in free-slot preview for one appointment type and date range; returns available slots only, with existing appointments used only as blockers
  • /api/appointments/days
    • GET: signed-in per-day free-slot availability (days array of { date, free_count } plus the resolved from/to/horizon_end range) for one appointment type; used for calendar-style day pickers
  • /api/appointments/appointments
    • GET, POST, PATCH /api/appointments/appointments/[id]/status, DELETE /api/appointments/appointments/[id]: signed-in appointment listing, internal booking, status update, confirmation, completion, and cancellation. Status values are pending, booked, cancelled, and completed; confirming a pending appointment by setting status: "booked" sends the customer confirmation email when the agent mailbox SMTP settings are configured.
  • /api/public/book-appointment/config, /api/public/book-appointment/days, /api/public/book-appointment/slots, /api/public/book-appointment/appointments
    • unauthenticated public embed API for active appointment types, per-day free-slot availability, free slots, and requesting one selected free slot. Public bookings require customer_email, create pending appointments, and independently send the customer receipt and configured internal team notification. Delivery results are stored under metadata_json.appointment_notifications; errors are credential-redacted, logged, and visible in the Termine module. The endpoint never returns existing appointments or customer records.
  • /embed/book-appointment
    • public iframe-ready booking UI for websites with a month calendar for the date and a time grid for the selected day; use a width up to about 1040px and a height around 680px to show the date/time picker and contact details side by side

Native agent tool proxy appointment contracts:

  • appointments_list_types: public-safe list of active appointment types, durations, and optional prices
  • appointments_list_free_slots: public-safe free-slot lookup
  • appointments_list_free_days: public-safe per-day free-slot availability for day/week overviews
  • appointments_book: public-safe booking mutation for a selected free slot; requires customer_email, creates a pending appointment request, and triggers the request-received customer email when mail transport is configured

Athlete-Brand Matching module API highlights under /api/modules/athlete-brand-matching/api:

  • GET /state: returns the signed-in user's module-local athlete, brand, review, follow-up, and outreach state plus persistence metadata
  • PUT /state or POST /state: replaces the signed-in user's module-local state; used by the iframe after manual capture, CSV/JSON import, review decisions, matching/outreach updates, and follow-up edits

Excel Editor module API highlights under /api/modules/excel-canvas/api:

  • GET /docs/:id: returns raw cells plus a workbook-aware sheetSnapshot payload for merges, hidden rows/columns, style metadata, comments, hyperlinks, and unsupported feature warnings
  • PATCH /docs/:id: accepts plain cell updates or workbook-aware operations[] batches. Operation vocabulary: set_cells, set_styles (target ref/range/cells plus style and optional full replace), merge, resize, hide_show, insert_delete, and set_pane (xSplit, ySplit).

Agent Orchestrator module API highlights under /api/modules/agent-orchestrator/api:

Native Agent Orchestrator runs survive runtime restarts through durable reconciliation. Before replaying an interrupted coding turn, the runtime compares its linked session/job, pre-turn workspace commit, current commit, pushed remote branch, and the forge's pull-request state. A PR is reused only when its head matches the changed workspace commit and its forge update timestamp is at or after the interrupted turn began; the run is then finalized as completed. Default-branch CI publication remains module-owned, so an unpushed local repair is not replayed as though publication could continue inside the write-disabled coding session. Forge lookup preconditions and every non-success HTTP response remain unavailable evidence rather than being reported as a conclusive missing PR.

Default-branch CI sessions persist a timeout recovery envelope as agent_external_sessions.metadata.mainCiAutofixCheckpoint. The envelope records phase, reason, turnId, capturedAt, workspaceDir, gitHead, gitStatus, bounded gitDiff, lastValidation, tool-failure counters, disposition: "pending", and deterministic continuation.resume / continuation.discard guidance. Runtime events expose the same transition as orchestrator_session.main_ci.checkpoint, followed by orchestrator_session.main_ci.finalization_requested or orchestrator_session.main_ci.finalization_failed.

  • GET /tools
  • POST /repos
  • GET /status
  • GET /poll
  • POST /config
  • GET /webhook/:token
  • POST /webhook/:token
  • POST /orchestrator/start
  • POST /orchestrator/stop
  • GET /remote-runners
    • Runner entries include cooldowns: [{ domain, cooldownUntil, permanent, failures, reason }] for persisted runner/provider circuits. Transient failures back off from 15 minutes to four hours. Independent runners retain their own provider availability.
    • Orchestrator GET /status tracked-PR failure entries include attempts and retryAt; repeated failures of the same trigger back off from 30 minutes to four hours across restarts and supervisor passes. An entry for a follow-up stopped by a runner host fault additionally carries hostBlocker (disk, memory, docker, or runner_offline) and blockedRunnerId.
  • POST /remote-runners/security-preflight
  • POST /remote-runners/heartbeat
  • POST /remote-runners/claim
  • GET /remote-runners/[runnerId]/codex-sessions
  • GET /remote-runners/[runnerId]/codex-sessions/[sessionId]
  • POST /remote-runners/[runnerId]/codex-sessions/[sessionId]/follow-up
  • POST /remote-runners/[runnerId]/install-actions with { kind: "install_codex" | "install_clapilot_code", preserveAuth?: boolean }; requires an administrator session or trusted agent-system credential and queues a hub-side install action the pull-based runner executes on its next poll. Runner machine credentials cannot invoke this cross-machine administrative endpoint. Requires codex-remote-runner/0.2.7 or newer (409 with REMOTE_RUNNER_UPDATE_REQUIRED otherwise), returns 409 while an action of the same kind is pending, and is blocked by the shell-tools kill switch. install_clapilot_code mints a dedicated revocable inference:execute instance API key named remote-runner:<runnerId> (rotating prior keys of that name; the key records remote_runner_id / remote_runner_label so Settings -> API keys shows the machine) and ships the CLI source inline; install_codex includes the hub's stored Codex auth when available. With preserveAuth: true, the hub sends no provider credentials and the runner preserves its existing login. Runner 0.3.4+ always upgrades Codex to @openai/codex@latest using npm in a user-owned ~/.clapilot-fleet/codex-cli prefix, which takes precedence over stale system binaries; a failed upgrade is reported as a failed action. npm must be available on the worker. Responses and GET /remote-runners expose only { id, kind, status, error, detail, requestedAt, finishedAt } per action plus supportsInstallActions per runner — secret payloads appear only in runner-authenticated heartbeat/claim responses (installActions[]) and are discarded once the runner reports installActionResults[] in a heartbeat. Those responses also carry runnerUpdate: { version, source } when an idle codex-remote-runner/0.2.8+ runner is older than the hub's bundled runner script; the runner applies it only between jobs and exits for its supervisor to restart the new version. GET /remote-runners additionally returns autoProvisionClapilotCode, and POST /config accepts remoteAutoProvisionClapilotCode to auto-queue Clapilot Code provisioning for connecting runners that lack the capability.
  • POST /remote-runners/jobs/[id]/events
  • GET /jobs
  • POST /jobs; repository jobs accept forgeProvider, forgeIntegrationName, forgeBaseUrl, and cloneUrl, but authenticated remotes are derived server-side from the resolved named connection so caller-controlled origins never receive stored credentials. Detached jobs may set canonical provider: "clapilot-code" plus a non-subscription catalog model to run through Clapilot's in-process coding loop (pi, embedded_pi, embedded-pi, and clapilot_code remain accepted aliases), detached codex or clapilot-code jobs may set executionTarget: "remote" (with auto resolving to codex) so a connected remote runner advertising the matching harness capability claims the work over the pull-based remote-runner API, and Codex or Claude jobs may set codexGoalEnabled: true to prepend /goal <task goal> to the first turn. Local Codex, Claude Code, and Clapilot Code jobs receive the restricted coding_core tool profile: repository shell/edit capabilities plus read-only Clapilot context, memory, Knowledge, Learning, search, and status tools. The only business mutation is aufgaben_add_comment, and it succeeds only when the coding session is bound to the exact originating Symphony task; memory writes, generic catalog dispatch, and every other business-action tool remain excluded
  • GET /jobs/[id]
  • GET /jobs/[id]/stream; local Codex, Claude Code, and Clapilot Code jobs all emit the same structured job-log events (itemType: agent_message | command_execution | file_change | mcp_tool_call | dynamic_tool_call | web_search with eventType, itemId, itemStatus, item). Claude jobs additionally run with --include-partial-messages: partial assistant text streams to SSE listeners as transient item.updated events for the same itemId (throttled, not persisted in logs), and only the completed agent_message entry is stored and replayed on reconnect. Codex item.updated agent-message partials are relayed the same way when the CLI emits them
  • GET /jobs/[id]/workspace-files?q=...&limit=... to search bounded, Git-ignore-aware relative paths in an owned local job workspace. Linked jobs use the linked session cwd; remote jobs return unavailable
  • GET /jobs/[id]/workspace to inspect an owned local job workspace through a bounded read-only snapshot: relative file inventory plus Git branch, porcelain status, unstaged diff, and staged diff. The endpoint accepts no command input, applies trusted-root and ownership checks, and is unavailable for remote jobs
  • POST /jobs/[id]/follow-up to continue a detached job with another prompt. Session-backed jobs resume the linked background session (the session runtime serializes concurrent turns natively); plain CLI jobs, including Claude CLI jobs, run the follow-up in the original job workspace. Remote Codex jobs are requeued for the same remote runner workspace. Local jobs accept text, optional attachments[], validated relative fileReferences[], or a combination; remote jobs reject file references because their workspaces are not server-local
    • Follow-ups sent while a remote or plain-CLI job is still running/queued no longer return 409: the prompt is appended to the job transcript immediately, stored in a per-job pending queue (max 20 entries, persisted across restarts), and the endpoint returns 202 with { job, turn: { status: "queued", text: "" } }. Queued entries dispatch in order after the current run reaches succeeded or failed; a user-initiated stop does not auto-dispatch, but the queue resumes with the next explicit follow-up request. Queued fileReferences[] are validated at request time and re-resolved against the workspace at dispatch time; a model override is honored for local CLI dispatches (remote dispatches keep the job model, matching the immediate remote path). A full queue returns 409 with code follow_up_queue_full
    • Serialized jobs (GET /jobs, GET /jobs/[id], follow-up responses) include queuedFollowUps, the number of pending queued follow-ups, so clients can render a queued indicator
    • 409 responses remain for genuinely unsupported cases: remote jobs on non-Codex/non-Clapilot-Code providers, local jobs whose provider supports no CLI follow-ups, closed linked sessions, and invalid workspace file references
  • DELETE /jobs/[id]
  • GET /sessions?view=summary|full&limit=...; summary is the default and omits embedded event/chat-history arrays while keeping compact status, model, activity, and latest-output previews. view=full remains available for compatibility and diagnostics; clients should load one selected session through its detail endpoint instead of polling full lists
  • POST /sessions with the initial session turn payload; accepts text, optional attachments[], or both, canonical provider: "clapilot-code" plus model for the internal embedded_pi adapter, plus optional codexGoalEnabled: true for Codex or Claude sessions. Repository sessions also accept forgeProvider, forgeIntegrationName, forgeBaseUrl, and cloneUrl; named-connection and GitLab sessions use the embedded runtime so provider credentials remain session-scoped.
  • GET /sessions/[id]
  • DELETE /sessions/[id]
  • GET /sessions/[id]/workspace-files?q=...&limit=... to search bounded, Git-ignore-aware relative paths in the owned local session cwd
  • GET /sessions/[id]/workspace to inspect an owned local coding-session workspace through the same bounded read-only file and Git snapshot contract
  • POST /sessions/[id]/turns to continue an interactive session; accepts text, optional attachments[], validated relative fileReferences[], or a combination. Referenced files are inspected from the active cwd rather than uploaded into the turn; the request is rejected if a selected path is stale or no longer resolves inside that cwd
  • GET /sessions/[id]/stream; optional replay=0 sends the initial session snapshot without re-emitting every historical event after it, which is the preferred selected-session reconnect contract
  • POST /sessions/[id]/fork
  • POST /sessions/[id]/archive

Remote runner heartbeats may include activeJobIds[], authenticated active claim proofs as activeJobs[] entries shaped like { id, claimToken }, codexSessions[], codexSessionsScannedAt, codexSessionScanError, and codexSessionDetails[]; heartbeat and claim responses may include cancelJobIds[]. Every session summary and nested detail session carries harness: "codex" | "clapilot-code"; the server defaults an absent field from older runners to "codex". Active claim proofs let the module reconcile an in-flight remote assignment after its own process restarts without accepting a claim from a different runner. Heartbeat and claim payloads carry capabilities[]: the macOS runner and Node runners up to 0.2.5 send ["codex"], while codex-remote-runner/0.2.6 and newer always include "codex" and add "clapilot-code" when the Clapilot Code CLI resolves on the machine. Fresh remote assignments are matched by that capability list; claim responses for Clapilot Code jobs carry provider: "clapilot-code", command: "clapilot-code", and arguments: ["exec", ...optional ["--model", model], task]. Runners 0.2.9+ add --output-format stream-json when the local CLI is 0.3.1+; the module parses that NDJSON into the same typed job-log events as Codex (agent_message, command_execution, dynamic_tool_call), while plain text from older CLIs is stored as raw stdout lines. Cancellation-aware Node runners identify as codex-remote-runner/0.2.2 or newer, and the macOS runner identifies as clapilot-remote-runner-mac/0.2.0 or newer. POST /remote-runners/security-preflight returns 200 when a shell-policy transition is safe, or 409 with REMOTE_RUNNER_UPDATE_REQUIRED or REMOTE_RUNNER_CANCELLATION_PENDING while active remote work cannot yet be safely drained. The runner builds its session snapshot from state_*.sqlite rows plus CLI/Desktop history files under the selected Codex home (session_index.jsonl and sessions/**/rollout-*.jsonl), and from Clapilot Code JSONL sessions under ~/.clapilot-code/sessions or the configured override. The server stores the latest per-runner snapshot in memory. GET /remote-runners/[runnerId]/codex-sessions returns the latest session summaries for the selected machine. GET /remote-runners/[runnerId]/codex-sessions/[sessionId] returns cached transcript detail when available; otherwise it records a detail request and returns requested: true, then the pull-based runner includes the transcript excerpt in a later heartbeat.

codex-remote-runner/0.3.9 and newer add hostHealth to the full heartbeat and claim payload, shaped as { freeDiskBytes, totalDiskBytes, freeMemBytes, totalMemBytes, dockerOk, checkedAt }. Disk figures come from df -kP on the runner workspace and memory figures from node:os; the reading is cached for 60 seconds. The lightweight job-lease heartbeat omits the field deliberately, and the hub retains the last known reading whenever a payload omits it. Older runners and clapilot-remote-runner-mac/* never send it and are treated as unknown health.

POST /remote-runners/[runnerId]/codex-sessions/[sessionId]/follow-up uses the same authenticated module browser/admin context as the sibling remote-runner inspector calls and accepts { message: string, model?: string }. It returns { jobId, status }, creates a queued job pinned to the selected runner, and stores the scanned session cwd and harness for execution. Empty messages return 400; unknown runners or sessions return 404; an instance with CLAPILOT_SHELL_TOOLS_ENABLED=false returns 403 with CLAPILOT_SHELL_TOOLS_DISABLED. Resume assignments use mode: "resume-session", resumeSessionId, resumeCwd, resumeHarness: "codex" | "clapilot-code", and reuseWorkspace: false. Codex assignments invoke codex exec resume; an explicit request model is forwarded while an omitted model preserves the session model. Clapilot Code assignments use command: "clapilot-code" with arguments: ["exec", "resume", sessionId, prompt]; the runner resolves the installed launcher, adds --cd, and relies on the model already saved in the session. Codex resumes require codex-remote-runner/0.2.3 or newer, Clapilot Code resumes require codex-remote-runner/0.2.4 or newer, and clapilot-remote-runner-mac/* is excluded from both.

GET /status now also returns richer tracked-PR follow-up observability, including the latest tracked scan results plus recent handled/failure entries for guarded PR comment triage and follow-up dispatches. Each repoConfigs[] entry can include githubTriggerMode, githubWebhookPath, and githubWebhookUrl so Settings -> Agent Orchestrator can show a repo-specific tokenized GitHub webhook endpoint. Enabled repository automations may additionally carry prReviewModel, issueObserverModel, and mentionObserverModel objects shaped as { provider: "codex" | "claude" | "clapilot-code", model: string }; the concrete model implies the runtime harness, and Main-CI fixes reuse issueObserverModel.

GET /status also returns effectiveMaxConcurrentAgents and a capacity object with active and synchronously reserved run counts, available slots, the current blocking reason, and total/free/reserved/per-run memory figures in MiB. The configured maximum defaults to one safe local run. Every local Symphony or repository-automation dispatch is gated by the shared slot count and the instance memory reserve before asynchronous preparation begins; stopped CLI jobs terminate their complete process group so coding subprocesses cannot survive as orphan workers.

POST /orchestrator/start and POST /orchestrator/stop now persist the Symphony enabled state in app_settings.agent_orchestrator_symphony_enabled. GET /status returns enabled; when disabled, the poll loop and manual poll endpoint do not dispatch aufgaben candidates or retry queued Symphony tasks. GitHub automations are controlled separately by the per-repository automation matrix, so issue observer, PR review, mention observer, and tracked-PR follow-up can continue even when Symphony task-board dispatch is disabled.

Symphony aufgaben dispatch is scoped by the default board in app_settings.agent_orchestrator_symphony_task_board_id plus repo-specific task-board mappings stored in app_settings.agent_orchestrator_repo_automation_config. POST /config accepts taskBoardId/symphonyTaskBoardId for the default board and accepts repoConfigs[] entries with optional taskBoardId; GET /status returns taskBoardId, taskBoardName, observedTaskBoardIds, and repoTaskBoardMappings. When neither a default board nor a repo board mapping is configured, no aufgaben candidates are dispatched. Tasks in mapped repo boards inherit the GitHub repo from the board mapping and use that repository's concrete issueObserverModel for the coding job, including the implied harness; legacy rows without a model use the global issue-observer provider fallback. Tasks in the default board still need repo:owner/name in their description before Symphony starts a coding job.

Symphony repository tasks use local capacity first; once it is full or memory-constrained, Codex and Clapilot Code tasks can reserve compatible Fleet capacity. Remote runners must support the 0.3.1 checkout contract. Per-machine limits add capacity independently of the local limit; unsupported tasks stay queued for local execution. No remote failure starts an unreserved local fallback.

Container bootstrap does not start Symphony by default; deployments that intentionally want boot-time activation must set CLAPILOT_AGENT_ORCHESTRATOR_BOOTSTRAP_ENABLED=true.

Symphony repository tasks now require PR traceability in the generated pull request body: Requested-by: Symphony task ... plus Clapilot task: .../aufgaben/{id}. The URL uses the configured public app URL from app_settings.public_base_url or public URL environment aliases (PUBLIC_BASE_URL, CLAPILOT_PUBLIC_BASE_URL, NEXT_PUBLIC_APP_URL, CLAPILOT_EXTERNAL_URL) and falls back to the app-relative task path; internal service URLs such as CLAPILOT_BASE_URL=http://clapilot:3000 are not used for this public PR link. Symphony and issue-observer PRs must also include a ## Testing Instructions section with concrete validation commands or manual checks; the web PR live-browser check and iOS/Mac Codex E2E workflow extract that section from the PR body and pass it into their Codex prompt context. The job record retains symphonyTaskId, and local coding MCP session keys append :symphony-task:{id}. The aufgaben_add_comment contract accepts { id, comment }, requires that full UUID to match the scoped origin, and writes a deduplicated Symphony agent comment for progress, implementation evidence, blockers, or questions; shared-board writes publish an Aufgaben reload. After detecting an opened PR, Symphony also writes its canonical URL, PR number, and open status as a system comment on the originating task independently of optional tracked-PR registration. The visible PR-link comment is generated in the task creator's persisted UI language (de, en, or it), with private-board owner and German fallbacks. A dedicated transaction takes a typed UUID advisory lock before the URL lookup and insert, making retries and concurrent completion paths idempotent while different PR URLs accumulate.

Specialized agent task comments: aufgaben_add_comment also accepts the same { id, comment } contract in mention and self-acting heartbeat sessions resolved through the effective specialist scope. The tool must be allowlisted. The enabled agent and its self_acting_board_id are read from specialized_agents; the comment insert is restricted to tasks on that board. These scoped calls do not require user context. Other boards return SELF_ACTING_TASK_BOARD_MISMATCH; missing specialist/Symphony scope returns SYMPHONY_TASK_SCOPE_REQUIRED after the normal identity gates. Comments use the database agent name and autor_typ=agent. The Symphony origin-task restriction is unchanged.

For regression tasks with exact session.*.failed signatures, the persisted job record also retains the linked PR and observation phase. The three-hour production window begins at the confirmed merge timestamp rather than PR creation, is reconciled after runtime restarts, and prevents both job cleanup and task completion while it is pending.

POST /config persists one GitHub/GitLab automation matrix (repoConfigs) plus review, issue-observer, mention-observer, and CI settings. Each entry records forgeProvider, integrationName, and forgeBaseUrl in addition to the existing trigger and feature fields, plus the optional per-automation model objects described above. Legacy global prReviewProvider, issueObserverProvider, and mentionObserverProvider values remain accepted as fallbacks for older rows without concrete models. POST /repos accepts connectionKeys (github:<name> or gitlab:<name>) and returns the union with provider, source connection, clone URL, and base URL metadata; legacy GitHub integrationNames remain accepted. POST /webhook/:token accepts GitHub repository webhooks and GitLab project webhooks, normalizes their issue, pull/merge-request, discussion/note, check/pipeline, status, and push event families, and scopes dispatch to the matching provider and repository.

Issue observer runs now close a GitHub issue as soon as implementation is picked up and comment that the observer is working on it, preventing another scan from starting a second implementation for the same issue. Existing issue sessions are reconciled through the persisted agent_events.event_data.sessionId linkage, and active issue-session conflicts are treated as already in progress instead of reopening the issue. Manual/detached jobs that explicitly target a GitHub issue use the same issue-level lock and are rejected while an implementation session is active or once an open PR already closes the issue. When the run produces a PR, the observer comments the PR link on the already-closed issue; if the run fails and no PR can be recovered, it reopens the issue and applies the normal failure backoff.

Tracked PR follow-up comment triage can return follow_up, reply, or ignore. reply posts a GitHub PR conversation comment for direct questions, status requests, and clarification comments that do not require a code change; follow_up remains reserved for low-risk code fixes on the existing PR branch and may include a follow-up result reply_body so the orchestrator posts a PR conversation reply after the branch update.

Apple native share workflow endpoints:

  • GET /api/apple/share-targets/telegram returns approved Telegram group targets that the iOS share workflow can present after a user shares a file into Clapilot.
  • POST /api/apple/share-targets/telegram/send accepts authenticated multipart form data with approvalId, optional message, and files[]. The route stages the uploaded files briefly in the workspace, sends them through ClapilotAICore's approved Telegram channel delivery, and removes the temporary staged files after the delivery attempt.
  • The native Apple share workflow also presents Issue melden for image/screenshot imports. That path reuses POST /api/issue-reporter with multipart images[], native Apple page context, and the user-entered text as the issue summary/details. The dedicated web and Apple issue reporter screens additionally expose the optional affected-platform selector described above.

News module API highlights under /api/modules/news/api:

  • GET /items
    • lists persisted news tiles; accepts limit, search, source_kind, athlete_name, review_status, and refresh=true to force an RSS sync before reading
  • POST /items
    • creates or idempotently upserts one news item for manual workflows, automations, and agent tool calls; clipping payloads may include athlete_name, project_name, partner_name, rating (1-3), relevance, source_reach, article_type, review_status, report_preview_url, screenshot_url, and import_source
  • PATCH /items/:id
    • updates a news item and its Presseclipping metadata for manual review and report preparation
  • GET/POST /settings
    • reads or updates RSS auto-ingest behavior and feed import limits
  • GET/POST/PUT/DELETE /rss-sources
    • manages configured RSS sources for the module
  • POST /rss-sync
    • forces an immediate RSS synchronization run

Accounting module API highlights under /api/modules/accounting/api:

  • GET /bootstrap
    • returns Mandanten, Kategorien, linked Dokumente, period-filtered entries, and the current accounting report snapshot for the selected client
  • GET /categories
    • lists all accounting categories
  • POST /categories
    • creates a custom accounting category
  • PATCH /categories/:id
    • updates one accounting category
  • GET /entries
    • lists accounting entries with mandant_id, period_kind, year, optional month / quarter, and optional direction/type filters
  • POST /entries
    • creates one accounting row with category/document linkage plus netto/steuer/brutto values
  • PATCH /entries/:id
    • updates one accounting row
  • DELETE /entries/:id
    • deletes one accounting row
  • GET /reports
    • generates the current bookkeeping and VAT-style period report including totals, category split, and VAT-code breakdown

Finanzen / Steuer-Manager module API highlights under /api/modules/tax-manager/api (UStVA transfer aids and EÜR drafts; full calculation and field details live in the module documentation):

  • GET /
    • returns module identity, legal notice, and the active endpoint inventory
  • GET /bootstrap
    • ensures the global Steuerbelege document folder and returns its UUID as tax_folder_id together with Mandanten, the 50 most recent workspace-visible generations, per-user settings, and the legal notice
  • GET /settings, PUT /settings, POST /settings
    • reads or upserts the authenticated owner's { agent_model_ref }; empty values inherit the normal runtime default
  • GET /candidate-documents
    • requires mandant_id plus period selection and returns accessible beleg, rechnung, and ust documents for that Mandant and period
  • GET /overview-documents
    • returns all visible beleg, rechnung, and ust documents plus visible documents in the global Steuerbelege folder. Supports optional title search q, mandant_id, and limit (default 200, maximum 500), and returns tax_folder_id with each document's titel, type/date/amount/currency, Mandant identity, and folder flag
  • GET /generations, POST /generations
    • lists workspace-visible UStVA/EÜR generation history (shared plus the caller's own private generations) or creates a generation from { kind, period_kind, year, period_index?, mandant_id?, source, document_ids[] } with a heuristic line-item baseline
  • GET /generations/:id, DELETE /generations/:id
    • loads one generation with all source-document line items or deletes it with its dependent line items
  • POST /generations/:id/line-items
    • bulk-upserts per-document extraction/classification results and sets the generation to extracted; monetary fields are non-negative integer cents
  • POST /generations/:id/finalize
    • deterministically recalculates UStVA/EÜR totals and Prüfhinweise, stores report HTML, and sets the generation to finalized; it can be rerun after corrections
  • GET /generations/:id/html, GET /generations/:id/pdf
    • returns the finalized stored HTML or an on-demand PDF; both return 409 until finalization
  • GET /health
    • returns the lightweight module health status

Dedicated Steuer-Manager agent trigger (outside the bundled-module API base, runs in the Next.js app context):

  • POST /api/modules/tax-manager/run-agent
    • body: { generation_id }. Requires an authenticated owner of the generation. Resolves the enabled steuer-manager specialist, creates a dedicated chat session, enqueues a background task, marks the generation extracting, and returns { ok, task_id, generation_id, chat_session_id, pending_message_id, agent }. The per-user module model is stored on the session and passed as the task fallback. Migration 007 clears the bundled specialist default; an admin-set specialist default intentionally takes precedence over the per-user fallback. The specialist reads every source document, submits classifications, and invokes deterministic finalization.
  • GET /api/modules/tax-manager/run-agent?task_id=…
    • authenticated status poll for the task returned by POST. Returns task timestamps/error state, running, tool_statuses, and an optional reply_preview for the generating step.

The current agent tools are tax_manager_list_generations, tax_manager_get_generation, tax_manager_submit_extraction, and tax_manager_finalize_generation. Their detailed schemas and live-voice exposure are documented in Agent Tool Contracts.

Native runtime machine-token scopes (for /api/modules/[slug]/api/...):

  • modules:api:read for GET/HEAD/OPTIONS
  • modules:api:write for mutating methods

Skill platform

  • /api/skill-store/local
  • /api/skill-store/catalog
  • /api/skill-store/publish
  • /api/skill-store/install
  • /api/skill-store/delete
  • /api/skill-store/agent-skill

GET /api/skill-store/local returns local/effective skill entries plus computed setup metadata:

  • requiredToolNames[]: optional specialist tool permissions declared by skill frontmatter
  • requiredAuthResourceKeys[]: optional specialist auth/API scopes declared by skill frontmatter
  • setup.status: ready, needs_setup, or disabled
  • setup.unmetRequirements[]: unmet env / app_setting requirements
  • setup.installRecipe: optional one-click setup action for the admin UI

POST /api/skill-store/delete deletes one local non-bundled, human-authored skill directory by id. Bundled and agent-authored skills are explicitly protected; agent-authored skills must be archived.

POST /api/skill-store/agent-skill is admin-only and updates the lifecycle of one workspace origin: agent skill by dir_name. It accepts status (active, draft, or archived) and/or pinned; it never deletes files or mutates human/bundled skills.

Widget platform

  • /api/mini-apps
  • /api/mini-apps/[id]
  • /api/mini-apps/[id]/data
  • /api/mini-apps/[id]/dashboard
  • /api/widget-store/local
  • /api/widget-store/catalog
  • /api/widget-store/publish
  • /api/widget-store/install

POST /api/widget-store/publish publishes the current structured widget snapshot as a versioned hub artifact.

POST /api/widget-store/install downloads a published widget snapshot, creates or updates the shared local catalog entry by slug, and installs it for the current user while preserving that user's dashboard placement on re-install where possible.

Specialized agent platform

Public visitor session isolation

Public-agent messages without sessionId start a fresh visitor conversation. Returned sessionId is an opaque, signed capability bound to the embed deployment and API key, valid for fourteen days. Clients send it unchanged for continuation; invalid, expired, raw legacy and cross-key identifiers return 401 before execution. Both streaming and ordinary messages use the same policy. The widget uses a new storage namespace, so legacy shared conversations are not resumed.

OpenAI-compatible /api/v1/chat/completions and /v1/chat/completions return the same capability contract through X-Clapilot-Session-Id (and clapilot_session_id for JSON). Send that header unchanged to continue. Missing headers start a fresh conversation; user, published API-key session IDs and message hashes never select conversation history. Invalid or expired headers return 401 before streaming starts. Streaming headers identify the actual runtime conversation. Quotas and logs use its verified internal identity.

  • /api/specialized-agents
  • /api/specialized-agents/[id]
  • /api/agent-store/catalog
  • /api/agent-store/publish
  • /api/agent-store/install

POST /api/agent-store/publish serializes a specialist into a versioned hub JSON snapshot for the Store's Special Agents tab and the admin specialist settings. POST /api/agent-store/install downloads that snapshot and creates or updates a local specialist by handle, while keeping deployment-specific access such as embed API keys and permission bypass out of the imported state.

Mixture of Agents

Admin-only. Manages moa/<slug> virtual-model presets that combine one aggregator model with N reference models. See Mixture of Agents.

  • GET /api/agent-runtime/moa-presets → { presets: AgentMoaPreset[] }.
  • POST /api/agent-runtime/moa-presets with { slug, label, enabled, aggregatorModelRef, referenceModelRefs, settings } → { preset }. Returns 400 for invalid input (bad slug, missing aggregator/references, or a MoA ref used as aggregator/reference).
  • DELETE /api/agent-runtime/moa-presets?slug=<slug> → { ok: true, slug }.

All three proxy to the native runtime's GET/POST/DELETE /internal/moa-presets. AgentMoaPreset.settings carries referenceMaxTokens, maxReferenceTurns, referenceTimeoutMs, synthesisInstruction, and exposeReferenceOutputs. Enabled presets are surfaced in listModels() as selectable moa/<slug> models.

Model Routing

Admin-only. Manages route/<slug> virtual models used by native request classification and model selection.

  • GET /api/agent-runtime/routing-models → { routing_models: AgentRoutingModel[], capabilities: string[], properties: string[] }.
  • POST /api/agent-runtime/routing-models with { slug, label, description, enabled, members, settings } → { routing_model }. Each member has { model_ref, capabilities: string[], properties: string[], note: string }.
  • DELETE /api/agent-runtime/routing-models?slug=<slug> → { ok: true, slug }.

The capability and property arrays returned by GET are the runtime taxonomy the settings UI uses for its member chips. Enabled Routing Models are exposed to model catalogs as route/<slug> when global Routing Models are enabled. Management through agent/chat tools is intentionally not exposed in this iteration; this API backs the admin web UI only.

Agent-focused usage map

  • documents: /api/documents, /api/documents/upload, /api/documents/inbox, /api/documents/folders, /api/documents/folders/[id], /api/documents/[id], /api/documents/[id]/preview, /api/documents/[id]/analysis
  • mini apps: /api/mini-apps, /api/mini-apps/[id], /api/mini-apps/[id]/data, /api/mini-apps/[id]/dashboard
  • emails (user): /api/emails*, /api/drafts*
  • emails (agent): /api/angela/emails* and scripts/agent-email-poller.mjs
  • modules: /api/module-store/* and /api/modules/[slug]/*
  • cases module: /api/modules/cases/api/*
  • call agent: /api/call-agent/*

Detailed flow and tool contract:

Internal native runtime

Native embedding compatibility endpoint

POST /v1/embeddings on ClapilotAICore requires x-clapilot-agent-secret with the configured native internal token. Missing configuration or invalid credentials return 401 before parsing input or calling a provider. Health and model discovery retain their existing contracts.

The native clapilot-agent service exposes internal-only endpoints used by the app runtime client:

  • GET /health
  • GET /metrics
  • GET /internal/models
    • returns runtime providers, memory description, and chat model entries; model entries include runtimeProvider and supportsSteering
  • GET /internal/provider-models?slug=<provider-slug>
  • GET /internal/sessions
  • POST /internal/sessions/model
  • POST /internal/chat/completions
    • accepts optional idempotencyKey / messageId (also clientMessageId and snake_case variants) as the durable run key; without one, a turn whose request explicitly sets sourceType to chat, group_chat, or channel receives the fallback key restart-recovery:<run id> so its restart recovery envelope is always persisted (the run id is runtime-generated here, so this fallback key adds no pre-execution idempotency lookup); requests that omit sourceType or use a helper value (chat_session_title, calendar_prefill, ...) are persisted as chat but stay non-recoverable
  • POST /internal/responses
    • accepts the same optional idempotencyKey / messageId run key as /internal/chat/completions
    • input[] entries must be typed message; untyped entries are ignored
    • input_image parts accept either source: { type: "base64", media_type, data } or a data-URL image_url (string or { url })
    • returns 400 { error: { code: "empty_input" } } when no non-system message carries text, image, or file content, instead of running the model against an empty conversation
  • POST /internal/runs
    • streams NDJSON lifecycle, tool, assistant, and completion events for the native agent loop
    • accepts optional idempotencyKey / messageId as the durable run key; a turn that explicitly sets sourceType to chat, group_chat, or channel and has no key receives the fallback key restart-recovery:<run id>, so every user-facing turn is recoverable after a runtime restart (see the session docs on run lifecycle reconciliation); requests without an explicit sourceType stay non-recoverable
    • a caller-supplied runId therefore becomes a real idempotency key for such a turn when no idempotencyKey / messageId is sent: repeating the same runId on the same session while the first run is still queued/running is rejected as an in-progress duplicate, and after completion it returns the persisted result instead of executing again; send a fresh runId (or omit it) per logical turn. Runtime-generated run ids skip the pre-execution idempotency lookup because their fallback key cannot match a persisted row.
    • disallowedToolNames is merged into the run's module gate set, so the listed tools are removed from every tool surface the model sees and denied at execution time; the boot reconciler uses the same path to block external side-effect tools that already ran before a restart and the shell/command tool family after an interrupted non-read-only command
    • accepts optional servicePrincipalId / servicePrincipalSlug alongside userId so execution identity can differ from the persisted room/thread history
    • accepts optional timeoutSeconds to place an explicit per-run cap on native execution; when omitted, the Claude CLI bridge path is no longer hard-limited to 180s
    • current native tool events may include exec_command, package_install, web_search, context_search, context_get, memory_search, memory_get, memory_grep, memory_describe, memory_expand, knowledge_search, knowledge_get_entity, knowledge_neighbors, knowledge_explain_claim, learning_search, learning_get_object, session_status, tool_catalog_search, tool_catalog_expand, tool_execute, and Clapilot-owned mutation tools proxied through the app/runtime bridge
  • POST /internal/runs/steer
    • internal-only direct steering endpoint for a currently running hidden Codex subscription bridge turn, Claude CLI bridge turn, or native/embedded-PI run
    • accepts { sessionKey, message?, attachments? }, locates the matching :subscription-bridge:codex orchestrator session, and forwards text/base64 attachments through Codex app-server turn/steer
    • for Claude subscription bridge runs, writes a realtime user-message event to the active Claude CLI stream-json stdin pipe
    • for native/embedded-PI runs, appends a LIVE USER STEER notice to an active tool result or, when the provider is generating without a tool call, aborts the current provider HTTP request and immediately continues the same run with the new instruction; the original NDJSON stream stays open through that continuation so pending animation and final delivery remain intact
    • returns 409 when the current run is idle, in its transition/finalization gap, or otherwise lacks a steerable active turn; native transition responses use reason=native_boundary_transition and are deliberately not acknowledged so the browser queue cannot lose them
  • POST /internal/runs/abort
    • internal-only user-stop endpoint for whatever run currently backs a chat session key
    • accepts { sessionKey }; interrupts a running :subscription-bridge:codex orchestrator turn through Codex app-server turn/interrupt, aborts an active native/embedded-PI provider request (or flags an active tool call to stop at its boundary) and finalizes it as cancelled (error_code = user_abort), and sweeps every queued/running agent_runs row for the session key — covering runs whose executing process died in a runtime restart
    • after requesting cancellation, waits for the exact captured live run to terminate. Confirmed termination returns HTTP 200 with { ok: true, aborted: { orchestrator, native, dbRuns, terminated: true } }; an unconfirmed tool/provider stop or failed persistence sweep returns HTTP 409 with { ok: false, aborted: { orchestrator, native, dbRuns, terminated: false }, error }. Both outcomes log a run.aborted agent event.
  • GET /internal/learning
    • returns passive learning-object dashboard data: recent objects, recent audit events, and grouped stats
  • GET|POST /internal/learning-objects
    • lists or creates durable facts, procedure/skill proposals, and hot memory snapshots in the native learning ledger
  • POST /internal/learning-objects/retrieve
    • internal runtime retrieval path for prompt-ready approved learning objects; enforces approval, expiry, visibility, subject matching, per-run limits, and a token budget before returning the Approved learned context prompt block
  • POST /internal/learning-objects/extract
    • internal diagnostics/runtime path for conservative post-response extraction; creates canonical durable facts or low-risk procedure drafts from shared-fact/explicit-memory input. The opt-out policy activates eligible evidence-backed facts and preferences immediately; exceptions remain candidates.
  • POST /internal/learning-objects/curate
    • internal diagnostics/runtime path for the targeted deterministic safety pass. Optional assertionIds / assertion_ids limits the scan to Learning projections for those canonical assertions and returns targetedAssertionIds; Dreaming v2 uses this targeted path immediately after each completed audience partition. Supported Dream assertions receive system-policy approval, while unsupported Dream assertions receive a terminal system-policy rejection instead of remaining in the human-review queue. The protected scheduled Learning Curator uses a separate model-backed runtime path configured through the job payload fields model, batchSize, and minimumRejectConfidence.
  • GET /internal/learning-objects/:id
    • returns one learning object with linked approvals and audit events
  • POST /internal/learning-objects/:id/decision
    • records approval-state decisions and appends matching audit events
  • GET /internal/learning-objects/:id/audit
    • returns audit events for one learning object
  • GET|POST|PATCH|DELETE /internal/jobs
    • scheduled automations only; heartbeat jobs (job_type = 'heartbeat') are owned by the heartbeat module and excluded from this listing
  • POST /internal/heartbeat/trigger
    • runs a configured heartbeat immediately with { scope: "user" | "teamchat", userId? } and returns { ok, delivered, suppressed, text, outputPreview, runId }; the regular interval schedule is left untouched
  • GET|POST /internal/orchestrator-sessions
  • GET|DELETE /internal/orchestrator-sessions/:id
    • DELETE aborts an embedded native run by its session key before archiving the session or cleaning its workspace. Unconfirmed termination returns HTTP 409 and leaves the session open; runtime errors also prevent closing. Symphony keeps the job active and retains its retry claim until cancellation succeeds.
  • POST /internal/orchestrator-sessions/:id/turns
    • accepts persistent goal control messages (/goal <objective>, status, pause, resume, clear). Goal work returns the final turn plus durable goal state and goalRuns; control-only responses return their goal_* status and formatted state. Goal judging and continuations are emitted through the existing session event stream.
  • GET /internal/orchestrator-sessions/:id/stream
  • POST /internal/orchestrator-sessions/:id/fork
  • POST /internal/orchestrator-sessions/:id/archive
  • POST /internal/channels/:channel/inbound
    • accepts the native telegram, slack, whatsapp, signal, imessage, and instance_bridge channel types
    • every delivery first takes an atomic per-event claim (agent_channel_inbound_claims, keyed by channel type plus the parsed eventId) before any slow work runs, so concurrent redeliveries of the same provider event cannot both start an agent run. A delivery that loses the claim returns 200 {accepted: true, duplicate: true, channel, eventId}, plus retryDelivery: true when the request carried Slack's x-slack-retry-num. The claim is a lease: it is released when processing throws, closed permanently once the event reaches its channel.inbound audit row, and reclaimable after 10 minutes if a process died before reaching either state
    • slack, telegram, whatsapp, and imessage respond asynchronously. Once the event is claimed and the approval/block decision is made, the endpoint answers 202 {accepted: true, queued: true, channel, eventId} and finishes transcription, thread upsert, attachment import, and the agent run in the background — provider retry windows (Slack retries after roughly three seconds) are far shorter than an agent run. Decisions that are already final at that point (channel disabled, unknown bridge, blocked approval, ignored payload) still return their terminal status and payload synchronously
    • because the provider has already received a 2xx, a background failure after the ack cannot be retried by the provider: the runtime releases the claim, logs the failure with an error reference, and sends a user-visible failure message to the inbound replyTarget on that channel
    • instance_bridge is deliberately excluded from the async ack: it is an internal Clapilot-to-Clapilot call whose caller expects the mirrored message and audit row to exist once the request returns, so it keeps its terminal synchronous response
    • instance_bridge payloads arrive pre-verified (the app route checks the fleet hub HMAC signature before proxying), are deduplicated by the payload eventId, and are matched against an existing approved bridge row; kind: "participant" messages mirror with the original sender name and may trigger the local main agent per the room's reply settings, while kind: "agent" messages mirror as agent messages and never trigger a local agent run
    • Telegram can stream natively: one visible reply message is created and then updated in place while deltas arrive
    • Telegram photo and document attachments are normalized into native input_image / input_file message parts before the run
    • Telegram forum/group topics now use chat.id:message_thread_id|root as the internal thread key, so agent memory/session scope is topic-aware while approvals still stay bound to the parent chat/group
    • approved group thread bindings may also carry the built-in global_team_service execution principal so the bot can run with service-level rights while preserving thread-local history
  • POST /api/agent-runtime/channel-mirror
    • internal-secret-only channel-to-Team-Chat display-copy endpoint for participant and agent messages
    • kind: "personal_turn" with { channelType, userId, sessionId, text, reply, attachments?, sender?, eventId? } stores a completed DM turn in the linked user's personal chat session (message_meta.channelOrigin, deduplicated by channelEventId); kind: "personal_outbound" with { channelType, userId, sessionId, text } stores an agent message sent into the DM from another session as an assistant_automation row. Both return { ok: true, messageId, sessionId } (duplicate for replays) and 404 when the session does not belong to the user
    • accepts optional attachments (maximum 10) with { type: "image" | "file" | "audio", name, mimeType, relativePath, size?, durationMs? }; relativePath must be workspace-relative and traversal-free
    • requires message text, at least one valid attachment, or agent uiActions[]; readable files contained by the configured workspace become URL-backed Team Chat attachments, and images receive the same URL as preview
    • agent requests with canonicalSource: true persist normalized assistantUiElements, omit the echo-blocking channelOrigin, synchronously reverse-forward the stored row, and report forwarded; this is the canonical Telegram reply path when metadata.mirror_to_channel is enabled
    • participant mirrors also dispatch room-invited specialized agents (one or more explicit @handle mentions → exclusive parallel dispatch to that invited target set, no mention → all all_messages invitees) and return { specialistDispatched, suppressChannelReply }; suppressChannelReply: true tells the channel runtime to skip its own default-agent reply for that inbound message
    • final-result-equivalent agent mirrors may set automationDelivery: true together with the persisted automation runId; only this explicit signal suppresses the mirror itself through durable run-and-room deduplication. Explicit tool sends from a scheduled run use automationRun: true with the same runId to tag each independently persisted mirror for later correlation; they do not reserve or suppress one another. A duplicate final result returns { ok: true, duplicate: true, run_id, roomId }. Normal channel runs may carry a run ID for tracing but never enter either automation boundary.
  • POST /api/agent-runtime/channel-audio-transcription
    • internal-secret-only bridge from clapilot-agent channel ingestion to the configured STT runtime
    • accepts a traversal-safe workspace filePath plus optional mimeType; the existing agent-media workspace guard rejects paths outside the shared workspace
    • returns the transcript and resolved provider/model metadata, while provider credentials remain server-side
  • POST /internal/channels/send
    • internal-only outbound send path used by ClapilotAICore tool proxy for approved Telegram/Slack/WhatsApp/Signal/iMessage/instance-bridge sends from normal chat runs
    • WhatsApp tool sends include mediaOwnerKey so every media[] item and local agent-media reference is resolved only against the provenance records for the active session actor
    • instance_bridge sends resolve the bridge approval row, build the signed bridge message payload, and POST it to the peer instance's /api/agent-runtime/channels/instance_bridge/inbound; bridge targets receive raw text plus structured sender fields instead of the [Name via Clapilot] / [AgentName] text framing, and attachments degrade to name-only placeholders in v1
    • a WhatsApp send to an unapproved international phone number returns { ok: true, approvalRequired: true, pendingApprovalId, queuedMessageCount, targetLabel } after queuing the message on a pending DM approval row; a send to a denied number fails
    • successful sends return { channel, recipient, text, mediaCount, failedMedia, targetLabel, approvalId, subjectKey, groupRoomId, mirroredRoomId, forwarded? }; failedMedia lists WhatsApp attachments that failed after text delivery, groupRoomId identifies the Team Chat room mapped by the matched approval, and mirroredRoomId is present only after the outbound message was actually persisted in that room. forwarded confirms the synchronous canonical Team Chat-to-channel delivery. The channel_send_message tool proxy separately normalizes these fields to snake_case for its public tool result.
    • messageOrigin: "team_chat_forward" sends to the provider without running the normal outbound Team Chat mirror; Telegram payloads containing both text and media[] send the text first and then the media, while other origins retain media-first ordering. For a Telegram channel-originated send whose approval has metadata.mirror_to_channel=true, the endpoint instead persists the reply and available current-run UI actions in Team Chat first and lets that row perform the provider send. A Team Chat forward may include telegramCopilotCard and sourceMessageId; the provider projection adds readable card content plus signed inline choice buttons. WhatsApp tool sends and automatic replies accept PNG/video/document agent media and auto-attach verified .clapilot/agent-media references instead of exposing server paths in visible text.
    • every successful explicit tool call is mirrored independently, including multiple sends from one scheduled automation. Structured send metadata travels separately from the truncated human tool preview and is retained across native stream recovery. Persisted Team Chat mirrors carry the stable automation run ID; after an ambiguous mirror timeout, the automatic final-result path waits briefly for that row and suppresses itself only if the row appears. If it does not appear within the bounded wait, the configured final result is delivered normally. Database-backed final-result delivery reservations carry a lease (CLAPILOT_AUTOMATION_DELIVERY_LEASE_MS, default 15 minutes) and a replayable payload snapshot; they can be reclaimed after failure or once the lease expires. The web server's automation delivery recovery worker claims lease-expired reserved rows exactly once (row lock plus lease renewal) and finalizes each as delivered when the chat row with the delivery key already exists, redelivers the stored payload idempotently, or marks it failed with a lease diagnosis in error_message and the decision in recovery_outcome. Approved external-channel reservations are never replayed by the recovery worker and remain at-most-once.
  • GET /internal/channels/whatsapp/auth
    • returns native WhatsApp Web linking/runtime status including linked identity, auth dir, reconnect state, and any active QR-login session
  • POST /internal/channels/whatsapp/auth
    • accepts { action: "start" | "wait" | "logout", force?, timeoutMs? }
    • start generates a QR-backed login session and returns a qrDataUrl when pairing is needed
    • wait polls for scan completion against the active backend-owned login session
    • logout clears the persisted native WhatsApp Web auth state under .clapilotaicore
  • POST /internal/packages/install
    • internal-only structured installer endpoint used by ClapilotAICore tool proxy; accepts kind = apt|brew|node|go|uv plus package-specific fields and executes the install inside the clapilot-agent container
  • GET /internal/memory/knowledge-graph
    • internal/admin diagnostics path for structured knowledge graph search; accepts query, limit, optional dreamId, optional instanceKey (default default), scope = personal|team|channel|agent|all, userId for the personal scope, and required specializedAgentId for scope=agent. It returns { entities, claims, edges, diagnostics } from the matching instance's agent_knowledge_* rows. The active graph is maintained automatically from approved/current assertion projections; dreamId narrows claims/edges by source_refs and entities by matching evidence from that Memory Dreaming run. scope=all explicitly excludes agent-private rows.
    • agent bootstrap and agent-facing knowledge_* tools still use the runtime visibility model; this all-scope behavior is limited to the internal/admin diagnostics path
  • POST /internal/memory/knowledge-graph/extract
    • internal/admin diagnostic repair path for rebuilding/backfilling graph items from approved canonical assertions associated with a dream; accepts { dreamId, memoryIds?, model? }, prunes prior graph source refs/evidence for that dream before re-extraction, and returns inserted/upserted entity, claim, edge, source-memory, and cleanup counts. Normal graph maintenance does not require this endpoint.
  • POST /internal/memory/retention
    • internal/admin maintenance path for the memory retention sweep; accepts optional { enabled?, supersededAfterDays?, evidenceMaxPerItem?, contextMessagesEnabled?, contextMessagesAfterDays? } overrides and returns purge/prune counts; also runs automatically after each nightly Memory Dreaming job
  • POST /internal/memory/reembed
    • internal/admin maintenance path for the embedding backfill; accepts optional { limit? }, re-embeds memories whose chunks were stored with a different embedding strategy/model than the active provider, and returns { reembeddedCount, candidateCount, targetKey }; skipped when no embedding provider is configured
  • GET /internal/memory/profile?userId=<uuid>&refresh=<0|1>
    • internal/admin diagnostics path for the precomputed per-user memory profile; returns { profile } with staticFacts (user-entity knowledge claims plus durable user facts), dynamicContext (recent user memories), the composed profileText injected at bootstrap, build stats, and builtAt; refresh=1 forces a rebuild instead of serving the cached agent_user_profiles row
  • POST /internal/tools/execute
    • internal-only native tool execution endpoint used when internal Clapilot services need the same native tools as /internal/runs
    • currently covers exec_command, the workspace file tools read_file / edit_lines / write_file (hashline LINE:HASH anchors; see docs/content/agent-tool-contracts.md), web_search, context_search, context_get, memory_search, memory_store, memory_get, memory_grep, memory_describe, memory_expand, knowledge_search, knowledge_get_entity, knowledge_neighbors, knowledge_explain_claim, learning_search, learning_get_object, session_status, ask_smarter_model, tool_catalog_expand, and Clapilot-owned module/document tools proxied through /api/agent-runtime/tool-proxy, including notizen_* and notizen_duplicate_local. Broad context_search results use reciprocal-rank fusion, canonical assertion/topic identities, confidence tie weight, and a per-source reservation before the final limit.

Admin config for the native runtime is exposed through Clapilot itself:

  • GET /api/agent-runtime/config

  • POST /api/agent-runtime/config

    • providers[].provider_type additionally accepts cursor. Cursor rows store an encrypted Cursor User API key, use https://api.cursor.com for model discovery, and execute through the optional local Cursor ACP bridge rather than an OpenAI-compatible HTTP endpoint. Provider status reports the row as not configured when the key or cursor-agent executable is missing.
    • returns and stores { providers, channels, model_routing }; both methods also return warnings, including { providerSlug, providerLabel } entries when an enabled fallback chain has no enabled provider slug outside its own failure domain
    • each provider's explicit enabled value is authoritative. Disabled providers retain their encrypted credentials and model configuration but are excluded from model catalogs, selection, and fallback routing. Enabled providers may have no text models when they are used only for audio, image, embedding, or other non-chat capabilities; text defaults and fallback routing consider only compatible configured text models.
    • provider enable/disable transitions are recorded transactionally in agent_provider_config_audit_events with the provider slug, authenticated admin when available, change source, previous/next state, and model/label context
    • provider rows accept provider_type=ollama; local base URLs require no API key, while https://ollama.com uses the encrypted api_key. The optional ollama_session_cookie and clear_ollama_session_cookie fields update only the browser session used for Subscription Usage. Admin responses expose masked presence/hint metadata and never return the cookie value
    • successful config writes invalidate the Subscription Usage snapshot and last-good cache so newly saved, replaced, or removed provider credentials are applied on the next usage request
    • rejects enabled Codex OAuth provider rows that contain non-OpenAI chat models (for example grok-4.5); Grok chat models must be configured on an xai provider
    • saving an enabled OpenAI API-key or Codex OAuth provider performs a minimal write preflight against every configured chat transport that runtime routing can use (/chat/completions and/or /responses; Codex OAuth uses the Codex Responses base). Saving or activating an OpenAI-compatible provider performs a one-token /chat/completions capability probe for each configured model and persists token-free metadata.modelAvailability results. Definitive model-load failures exclude only that model from catalogs/default routing; inconclusive connectivity failures remain routable. A failed provider-wide preflight clears default eligibility.
    • each channel accepts enabled, allow_direct_messages, encrypted credential fields, and provider-specific settings; settings.mention_only = true makes approved group/channel traffic run only when Clapilot is explicitly mentioned
    • model_routing.priority is an ordered list of fully qualified model refs such as openai/latest for the newest Codex OAuth GPT coding model, claude-default/latest for the newest Claude subscription coding model (claude-opus-5), concrete pins such as openai/gpt-5.6-sol, openai/gpt-5.6-terra, openai/gpt-5.6-luna, openai/gpt-5.5, claude-default/claude-opus-5, claude-default/claude-fable-5-1, or claude-default/claude-fable-5, or provider-specific refs such as google-gemini-default/gemini-2.5-flash
    • model_routing.routing_enabled is a boolean feature gate for route/<slug> virtual models and defaults to false
    • model_routing.default_routing_model_slug is the Routing Model slug used when a request does not explicitly select a model; an empty string leaves the existing provider/model default path unchanged
    • model_routing.smartest_model is an independent fully qualified model ref used by the native ask_smarter_model tool. It is available even when routing_enabled=false. Eligible runs expose the tool only when their resolved active model differs from the configured smartest model. Each call sends only the tool's standalone question to a stateless, tool-free completion and returns { answer, model, active_model_unchanged: true }; it never rewrites the session's configured or effective model. Direct API providers, Claude subscription rows, and Codex OAuth rows use their existing reference-completion paths, and an unavailable exact model fails instead of silently falling back to a different model.
    • latest refs are moving Clapilot aliases resolved from the enabled provider catalog at execution time; concrete model IDs remain explicit pins
    • the first entry is the global default model for native runs; later entries are cross-provider fallbacks
    • model_routing.embedding_provider_slug and model_routing.embedding_model select the provider/model used for native memory embeddings and the document RAG indexer/retriever
    • model_routing.realtime_provider_slug and model_routing.realtime_model store the separate Realtime provider/model choice for live/call audio routing
    • model_routing.tts stores the global TTS provider slug, model, and voice used for async chat audio replies and other server-side speech synthesis defaults. Agent media_tts_speak accepts exact provider_slug and model overrides; explicit slugs must resolve to that enabled provider and are never replaced by the default. OpenAI-compatible providers are supported through their configured base_url plus /audio/speech; the model may be an exact gateway alias such as chatterbox, and authless compatible endpoints omit the bearer header. media_tts_speak failures return retryable, error_kind, upstream_endpoint, upstream_status, upstream_cause, and retry_after_seconds alongside the localized message; speech requests are bounded by CLAPILOT_TTS_REQUEST_TIMEOUT_MS and a failed endpoint is short-circuited for CLAPILOT_TTS_UPSTREAM_COOLDOWN_MS.
    • model_routing.stt stores the global STT provider slug and model used for chat audio uploads and note dictation transcription defaults. Agent media_stt_transcribe accepts exact provider_slug and model overrides and passes the resolved runtime config into the outbound request instead of resolving the default again. OpenAI-compatible rows call their configured base_url at /audio/transcriptions, accept an exact/manual model alias, send the stored key as a bearer token when present, and omit authorization for authless local endpoints.
    • model_routing.image_generation stores the global image-generation provider slug and model used by /api/generated-images/* and agent image tools. Both image endpoints/tools accept optional exact provider_slug and model overrides; explicit providers never fall back to the global default, while legacy recognized model-family overrides select the matching provider type. OpenAI API-key rows use /images/*; OpenAI-compatible rows call the selected provider base_url with the OpenAI-compatible /images/generations and /images/edits paths; an explicitly selected or edit-curated compatible provider is always used for edits (a missing endpoint yields a localized error), while uncurated OpenAI-compatible text-to-image defaults are skipped for edit calls only when no explicit provider was requested and the provider metadata does not mark image edits as supported. OpenAI-Codex OAuth rows use the Codex Responses image_generation tool with gpt-image-2.5-flare (default), gpt-image-2.5-sunburst, or explicitly selected older image models; the Responses host chat model is resolved from the provider metadata override imageGenerationResponsesModel, then CLAPILOT_CODEX_IMAGE_CHAT_MODEL, then the provider's configured Codex chat models (skipping latest), then the shipped defaults (gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4-mini). A host model the ChatGPT-account Codex backend rejects as unsupported (HTTP 400 "model is not supported when using Codex with a ChatGPT account") is skipped for the next candidate; only when every candidate is rejected does the call fail with a localized error listing the tried models. The stored asset metadata codex_responses_model records the host model that actually produced the image. Agent images_edit can import workspace-local source_image_path inputs before handing image bytes to the selected provider.
    • media_generation_provider_configs stores AI media provider configuration for video and music. app_settings.media_model_catalog stores curated video, image, and music arrays of { providerSlug, model, isDefault }; image entries additionally accept imageOperations (generate for text-to-image only, edit for image edit only, both; absent means both for display and keeps legacy routing heuristics), which GET /api/generated-images/models echoes and which gates generate/edit routing; saves require enabled compatible providers and normalize every non-empty list to exactly one default. A non-empty capability catalog overrides its legacy single default, while an empty list preserves the old behavior. Each enabled OpenAI-compatible runtime provider with a base URL is mirrored into the video-provider selector and uses the OpenAI Videos lifecycle: multipart POST /videos, GET /videos/{id} polling, and authenticated or authless GET /videos/{id}/content download. Those rows inherit the mapped runtime provider's base URL by default; the optional settings.videoBaseUrl overrides it for the whole video lifecycle, and settings.videoAuthDisabled suppresses the Authorization header. Use the override when the video backend is reachable directly but chat runs through a gateway — job ids issued by a gateway are encoded routing tokens and are not interchangeable with the backend's own ids, so submit, status, and content must all target one host. Both keys survive the automatic runtime-provider sync and are editable under Settings -> ClapilotAICore -> AI media -> Video generation. GET and POST /api/media-generation/providers return the catalog alongside provider rows, augment compatible video rows with available_models from the mapped runtime provider's live /internal/provider-models discovery, and augment xAI rows from /video-generation-models; model_discovery_error reports fallback to saved/manual models without invalidating the current selection. The chat-facing videos_generate/videos_status path uses the video catalog default when no explicit provider/model is supplied, accepts image_id/source_image_path for image-to-video, persists jobs and source-image lineage in generated_videos, and streams ready files through /api/generated-videos/[id]. livestream_generate_music applies the same rule to the music catalog. Explicit tool/API provider and model arguments still win. Live Stream Studio keeps using livestream_generate_video for livestream assets.
    • OpenAI-compatible video submit requests include the generated asset UUID in both Idempotency-Key and X-Request-ID. A 504 is reconciled through bounded GET lookups and never causes an automatic second POST. Provider settings submitReconcileEndpoint ({idempotency_key} or {idempotencyKey} placeholder), submitMaxAttempts, and submitRetryBaseDelayMs configure that lookup; the default endpoint is the configured submit endpoint followed by /{idempotency_key}.
    • model_routing.non_specialized_agent_core stores the default agent-core selection for providers without their own specialized bridge: native or embedded_pi; Agent Orchestrator exposes the latter as the clapilot-code coding harness
    • embedded_pi keeps the chosen provider/model transport, but runs normal agent turns with the selected runtime tool profile and expanded tool-loop budget; Agent Orchestrator coding runs explicitly select coding_core, which keeps shell/package primitives and the hashline file tools (read_file, edit_lines, write_file) plus read-only Clapilot recall and excludes shared-memory writes and business mutations
    • model_routing.adaptive_routing configures native per-request model/profile routing:
      • mode: off, shadow (default), or apply
      • candidate_limit: maximum number of ordered model_routing.priority entries considered, clamped to 2..8
      • exploration_rate: bounded exploration probability, clamped to 0..0.25
      • min_samples: observations required before learned outcomes receive their full configured influence
      • switch_margin: minimum score advantage required to leave the base/recent session model
      • route_harness: whether apply mode may choose between Clapilot-code Assistant (native) and Coding (embedded_pi)
    • explicit request/session model or profile choices, specialized agents, and Mixture-of-Agents presets bypass adaptive application. Shadow/apply decisions and technical outcomes are persisted in agent_adaptive_route_decisions.
    • routing-model (route/<slug>) selections are not bypassed but scoped: learned outcomes may reorder members within the routing model's tie band (bounded by min_samples and switch_margin, capability tags always win); the decision row records the routing model in routing_model_slug with the static pick as base and the outcome pick as recommendation
  • GET /api/agent-runtime/adaptive-routing/decisions

    • admin-only read endpoint backing the adaptive-routing data view in Settings -> ClapilotAICore -> Model Routing
    • accepts ?limit= (1–200, default 50) for the recent-decisions list
    • returns summary (total, last-7-days, outcome-status counts, protected count, recommended vs. applied switches, routing-model totals and agreement count), stats (per task class and selected model over the 90-day learning window: samples, completed, failed, average success score, average duration), and decisions (most recent rows from agent_adaptive_route_decisions incl. routing_model_slug, base/recommended/selected model, protection/decision reason, and technical outcome)
    • Codex OAuth provider metadata can include modelReasoningEfforts, a map from configured model id to an explicit Codex effort (minimal, low, medium, high, xhigh, max, or ultra). Missing entries keep the model's Codex-advertised default. Explicit per-session Agent Orchestrator effort overrides the provider value.
  • GET /api/agent-runtime/channel-approvals

    • returns pending, approved, and denied native channel approval records in approvals plus rooms, the admin's non-archived mappable public/private team-chat channels and group rooms, for the ClapilotAICore settings UI
    • also returns chatSessions ({ id, title, is_main }[], only the calling admin's own personal sessions) and currentUserId for the DM chat-session picker
  • POST /api/agent-runtime/channel-approvals

    • admin-only approval decision endpoint; accepts { id, status } where status is approved, pending, or denied
    • DM approvals should additionally send { linkedUserId, chatSessionMode?, chatSessionId? }; chatSessionMode is main (default, the linked user's Hauptchat), dedicated (get-or-create a per-approval session <Channel> · <contact>), or session (requires chatSessionId, which must belong to the linked user and — unless unchanged — the linked user must be the calling admin). The runtime then maps the external DM thread onto that session's key and stores metadata.chat_session_mode, chat_session_id, and chat_session_user. Invalid session choices return 400 with a localized error
    • group approvals can additionally send { groupRoomId, mirrorToChannel? }; non-empty room ids must identify an existing, non-deleted, non-direct team-chat room, and the runtime then maps the external group thread onto that room's session key, defaulting to clapilot-members; mirrorToChannel persists the opt-in reverse-mirroring flag as metadata.mirror_to_channel
    • already approved entries can be re-saved with a different mapping, or reset back to pending to block replies again without deleting the approval record
    • the response is { approval, outboundDelivery }. When the entry holds agent messages queued while the recipient was unapproved (metadata.pending_outbound_messages), approving it claims and sends them and returns outboundDelivery: { delivered, failed }; denying it discards them and returns { delivered: 0, failed: 0, discarded }; otherwise outboundDelivery is null (or zero counts)
  • GET /api/agent-runtime/channel-bridges

    • admin-only list of this instance's instance_bridge channel-approval rows (both initiator and peer roles) with resolved local room names, for the ClapilotAICore Channels settings UI
  • POST /api/agent-runtime/channel-bridges

    • admin-only bridge creation on the initiating instance; accepts { peerInstanceId, peerRoomId, localRoomId } where peerInstanceId is a hub_monitored_instances id or a fleet instance
    • the server resolves the peer base URL and label, generates the shared bridge_id, calls the peer's signed POST /api/hub/channel-bridge/register, and only on success creates the local approval row with role initiator; a failed peer call returns 502 and creates nothing locally
  • POST /api/agent-runtime/channel-bridges/peer-rooms

    • admin-only server-side signed call to the selected peer's POST /api/hub/channel-bridge/rooms; accepts { peerInstanceId } and returns the peer's mappable room list for the create dialog
  • DELETE /api/agent-runtime/channel-bridges/[id]

    • admin-only bridge removal; deletes the local approval row and sends a best-effort signed remove to the peer — peer failure is logged and the local delete still succeeds
  • POST /api/agent-runtime/channel-response

    • internal-only Clapilot channel execution bridge used by clapilot-agent; runs approved channel messages through the same Clapilot delegation path that powers broader app actions
    • this route executes directly through the native ClapilotAICore run path; legacy gateway fallback is compatibility-only
    • now fail-closed: requires a valid x-clapilot-agent-secret header and refuses requests when no internal secret is configured
  • POST /api/agent-runtime/channels/:channel/inbound

    • optional public webhook ingress for external channel providers; the app forwards the payload to native clapilot-agent /internal/channels/:channel/inbound
    • the proxy passes the native response status and body through unchanged, so slack, telegram, whatsapp, and imessage webhooks receive the async 202 {accepted: true, queued: true, channel, eventId} ack while processing continues in the background; instance_bridge keeps its terminal synchronous response. See the internal endpoint above for the full claim/ack contract
    • besides the internal secret and the iMessage webhook password, the proxy forwards x-slack-retry-num and x-slack-retry-reason so the runtime can tell a Slack redelivery apart from a first delivery
    • imessage accepts only BlueBubbles new-message webhooks and requires a password or guid query parameter matching the encrypted iMessage channel credential; mismatches return 403
    • instance_bridge is not a blind proxy: the fleet hub HMAC signature (x-clapilot-instance-id, x-clapilot-ts, x-clapilot-signature) is verified on the raw body before proxying, a bad or missing signature returns 401, and GET is not supported for this channel. Unknown bridges are rejected without creating pending approvals. The signed bridge message wire payload posted by the sending instance's agent service is:
{
  "bridgeId": "…",
  "originInstanceId": "…",
  "originRoomId": "…",
  "eventId": "<originInstanceId>:<chat_group_messages.id>:<participant|agent>",
  "kind": "participant" | "agent",
  "senderName": "Alexander Deutsch",
  "senderKey": "user:<uuid>",
  "agentName": "Clapilot",
  "text": "…",
  "attachments": [{ "name": "a.pdf" }],
  "sentAt": "ISO-8601"
}
  • agentName is present only for kind: "agent"; attachments carries names only in v1 (the receiving side renders 📎 <name> placeholders)
  • GET /api/agent-runtime/channels/whatsapp/inbound
    • public WhatsApp Cloud / Meta webhook verification handshake; validates hub.verify_token against the native channel settings JSON (webhook_verify_token, webhookVerifyToken, verify_token, or verifyToken) and returns the plain hub.challenge
  • GET /api/agent-runtime/channels/whatsapp/auth
    • admin-only native WhatsApp Web status endpoint used by the ClapilotAICore Channels settings UI
  • POST /api/agent-runtime/channels/whatsapp/auth
    • admin-only QR-login control endpoint for native WhatsApp Web; proxies start, wait, and logout actions into clapilot-agent
  • GET /api/agent-runtime/provider-models?slug=<provider-slug>
    • loads the current provider model catalog so the ClapilotAICore UI can populate the provider model dropdowns
    • the dedicated Audio and AI Media routing tabs consume this live provider catalog directly for realtime/TTS/STT/image model selection instead of reusing only the manually added chat-model entries. OpenAI-compatible TTS additionally keeps the model field editable, because gateway aliases do not have to contain a tts marker and some speech-only servers do not implement /models.
    • Gemini provider catalogs can feed both media routing and chat routing; chat selectors exclude obvious Gemini media-only model ids while text-capable Gemini models run through the native generateContent adapter
    • OpenAI Codex OAuth providers use the local Codex app-server model/list catalog for this lookup, including the current GPT-5.6 Sol/Terra/Luna lineup, the GPT-6.1 Sol, GPT-6 Astra, GPT-6 Sol, and GPT-6 Luna shipped fallback entries, GPT-5.5, GPT-5.4, GPT-5.4 Mini, subscription-only GPT-5.3 Codex Spark, advertised per-model speed tiers, supported reasoning efforts, and each model's default reasoning effort; OpenAI API-key providers still use the OpenAI /models endpoint and do not receive Spark as a shipped default
  • Anthropic providers backed by a Claude setup-token use the shipped Claude subscription catalog, including claude-opus-5, claude-fable-5-1, and claude-fable-5, and execute chat requests through the local Claude CLI bridge rather than direct Anthropic /v1/messages calls
    • the runtime now persists one hidden Claude bridge session id per Clapilot session_key, so follow-up turns resume the same Claude Code conversation until the session model is changed or cleared
    • AWS Bedrock providers use the Bedrock bearer-token/API-key path in the supported admin flow; the loader attempts to return both foundation-model ids and Bedrock inference profiles, but manual model entry may still be required when Bedrock control-plane discovery is unavailable on that auth path
    • Azure OpenAI providers normalize the configured resource URL onto /openai/v1, keep requests under that path, and only append apiVersion when it is a preview/date-style value instead of v1; the runtime accepts both Azure API-key auth and OpenAI-style bearer auth for Azure v1
    • when the upstream model list exposes token limits, Clapilot forwards optional per-model metadata such as contextWindow / outputTokenLimit
    • session execution does not call this endpoint live; it uses persisted provider config first, then the shipped OpenAI/Anthropic model-limit fallback catalog, and only then the generic 32,000 / 4,096 emergency defaults
    • provider discovery still understands legacy openai-codex/... refs, but Codex OAuth GPT models are exposed as canonical openai/gpt-* refs; standard native chat may select OpenAI-Codex, execute it through the Codex bridge, and now keep a hidden reusable Codex thread per Clapilot session_key for follow-up turns
    • direct OpenAI/Azure GPT-5.6 and GPT-6 API models (gpt-5.6, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-6, gpt-6-astra, gpt-6.1-sol) execute through /responses even for text-only turns; this preserves the supported reasoning-plus-tools path instead of using Chat Completions with GPT-5.6's reasoning default
    • provider config metadata may also include modelExtendedContextWindows, a per-model boolean map that re-enables the larger public OpenAI fallback profile for eligible Codex-auth models when admins explicitly turn it on
  • GET /api/agent-runtime/provider-status?slug=<provider-slug>
    • admin-only provider health endpoint for the ClapilotAICore settings UI
    • returns whether the saved provider is configured and whether auth/connectivity is currently working; failed OpenAI write preflights return status: "missing_scope" when applicable, plus missingScopes and a token-free repairHint; preflightReason is one of ok, missing_scope, auth_failed, endpoint_not_found (the configured transport endpoint answered 404, provider is excluded from routing), or inconclusive. OpenAI Codex setups are allowed to validate from the stored OAuth credential and do not fail just because a stale OpenAI API key is also present, AWS Bedrock setups validate against the configured region plus the stored Bedrock API key, and Azure OpenAI setups validate against the normalized Azure v1 endpoint plus stored API key
    • also returns the runtime timeout circuits of the provider: circuitOpen, circuitOpenModels, and circuits[] with modelRef, status (open | half_open | degraded), openUntil, nextProbeAt, consecutiveTimeouts, recentTimeouts, windowMs, threshold, lastReason, lastError, lastFailureAt, openTrigger, openedCount, reopenStreak (re-opens after a failed full-budget probe; each doubles the open window), openDurationMs, probeFailures (also lengthens the next bounded probe budget), fullBudgetTimeouts, fullBudgetTimeoutLimit, fullBudgetWindowMs, budgetBounded (the model burned its full-budget timeout allowance inside the long window, so requests with a fallback run under the bounded budget), boundedBudgetMs (the stall budget the next bounded request or probe receives before the conversation cap; null for a merely degraded circuit), restored (loaded from agent_provider_circuit_states after a runtime restart), and persisted (database mirror active and the restore succeeded); the in-memory state is mirrored into agent_provider_circuit_states so it survives a ClapilotAICore restart
  • DELETE /api/agent-runtime/provider-status/circuit?slug=<provider-slug>&key=<circuit-key>
    • admin-only operator reset of the provider timeout circuit breaker; key is optional and defaults to every circuit of the slug
    • clears the in-memory breaker and the matching agent_provider_circuit_states rows and returns { cleared: string[] }; the provider dialog in Einstellungen exposes it as a reset button per circuit
  • GET /api/agent-runtime/memory
    • returns native memory diagnostics including shared workspace path, detected bootstrap files, detected memory/**/*.md files, embedding backend/model, retrieval tuning, native pre-compaction memory-flush settings, sync state, current native DB layer/chunk counts, Memory v2 assertion/evidence/embedding/outbox health, visibility-scope counts, active/superseded memory-state counts, Memory Dreaming counts, structured knowledge graph counts, typed memory-relation counts, per-user profile counts, and lossless context graph counts
    • the Runtime Memory UI now explicitly distinguishes durable-memory ownership (Clapilot Native) from context-window maintenance ownership (native provider loop vs embedded_pi) and shows recent Memory Dreaming runs plus the Knowledge Graph inspector
    • the shared bootstrap-file editor uses this endpoint's promptFiles inventory but is shown on the dedicated Bootstrap-Dateien settings subpage next to Runtime Memory
  • GET|PUT /api/agent-runtime/bootstrap-files
    • admin-only read/update endpoints for the native runtime bootstrap/prompt files backing the Bootstrap-Dateien settings subpage
  • POST /api/agent-runtime/memory
    • runs an idempotent synchronization from the shared workspace memory/**/*.md tree into native agent_memories + agent_memory_chunks and refreshes the prompt-file projections; the response reports mode: "workspace", aggregate importedCount / updatedCount / deletedCount, the workspace file inventory, and promptFiles
    • the request body is ignored; the former mode: "compatibility" | "all" legacy transcript import and the compatibility response block were removed
  • GET /api/agent-runtime/memory/dreams
    • admin-only response { dreams, executions }: recent partition batches retain status, trigger, input/output refs, stats, model, and rollback state. executions contains the 12 latest whole runs with id, status, phase, progress, result, errorCode, and creation/start/completion/update timestamps. Status is queued|running|completed|completed_with_errors|skipped|failed|interrupted. A completed batch does not imply whole-run completion.
  • POST /api/agent-runtime/memory/dreams
    • admin-only manual Memory Dreaming v2 run; accepts optional { triggerKind, force, model, scopeKey, async }, validates strict structured output, applies the versioned deterministic automatic assertion policy, and creates every Wiki change only as a review-required draft
    • after each completed audience partition, the runtime immediately curates that partition's returned assertionIds before starting the next one and processes the matching projection work. Safe, durable, evidence-backed facts, preferences, decisions, constraints, and procedures can therefore reach recall and extend the Knowledge Graph without human review or the recurring curator schedule; unsupported, sensitive, transient, time-bounded, conflicting, or insufficiently supported Dream assertions are automatically rejected. The response includes an aggregate curation result plus partitionCurations. If a Dream persists but immediate curation/projection fails, the response keeps the Dream result and returns curation.ok=false; the Runtime Memory UI reports this as a partial failure instead of a successful graph build
    • async: true returns HTTP 202 { ok: true, created, execution } after durable enqueue. Concurrent requests reuse the active execution (created: false). The worker continues through curation and consolidation without retaining the initiating HTTP request. Poll GET for the outcome; 202 is acceptance only. Omitting async retains the synchronous result contract. The internal /internal/memory/dreams endpoint has the same behavior. Manual runs consolidate affected partitions only; scheduled runs may additionally sweep quiet partitions. Expired workers without the advisory lock become interrupted after lease expiry; queued work is resumed automatically.
    • an explicit model on a manual run may be a configured Codex or Claude subscription model and is honored exactly for that Dream and its graph extraction; scheduled runs continue to exclude subscription bridges, and unavailable explicit models fail closed without provider fallback
    • the Memory settings UI sends the current global priority model by default and can override it per manual run; malformed, empty, truncated, or ungrounded structured output is rejected and the partition is failed/skipped without a deterministic fallback or Wiki publication.
  • POST /api/agent-runtime/memory/dreams/{id}/rollback
    • admin-only rollback for one dream run. Dreaming v2 compensates candidates, system-policy activations that have not received a human decision, drafts, outbox items, projections, and conflicts. A human-reviewed assertion/proposal or a published proposal blocks rollback. Legacy dream-v1 rows retain the older inserted-memory removal and superseded-source restoration behavior.
  • GET /api/agent-runtime/memory/knowledge-graph
    • admin-only proxy for structured knowledge graph diagnostics; accepts query, limit, optional dreamId, scope = personal|team|channel|agent|all (default personal), and required specializedAgentId for scope=agent, returning entities, claims, edges, and coherence diagnostics for that inspector view. The proxy derives the personal owner id from the authenticated admin session rather than accepting it from the browser. The active graph is maintained automatically from approved/current assertion projections; the UI can also filter the selected scope to one Memory Dreaming run. scope=all explicitly excludes agent-private rows.
    • an empty-query overview prioritizes recently updated supported relationships and reserves nodes for distinct connected components before expanding dense clusters. Text searches retain relevance ranking; visibility and current-support checks are unchanged.
  • POST /api/agent-runtime/memory/knowledge-graph
    • admin-only diagnostic repair path for graph rebuild/backfill; forwards { dreamId, memoryIds?, model? } to the native runtime and is not required during normal automatic graph maintenance
  • Learning object contracts for durable facts, procedure/skill proposals, hot memory snapshots, approval/audit state, visibility scopes, and token-cost attribution now have admin/native storage endpoints, an internal approved-learning prompt retrieval path, read-only agent tools (learning_search, learning_get_object) for approved visible objects, unified read access through context_search / context_get, an internal conservative extraction path, opt-out safe fact/preference activation, and a configurable model-backed scheduled curator. There are still no agent-facing learning mutation tools; activation, curation, and exception review remain runtime/control-plane paths. See Clapilot-Agent Learning Contracts.
  • GET /api/agent-runtime/sessions
    • admin-only session diagnostics endpoint for the ClapilotAICore Sessions settings page
    • without query params, returns the runtime session list from agent_session_state enriched with chat-session mapping, user labels, runtime path / maintenance owner, last-run status, aggregate run/event counts, lossless context counters, shared-fact counters, memory-flush/compaction counts, last compaction marker, last observed prompt-budget telemetry, and chat_nachrichten message counts where a matching chat_sessions row can be resolved; channel-bound interactive Agent Orchestrator / Codex app-server sessions from agent_external_sessions are also surfaced when they share the same Clapilot session_key
    • with sessionKey=<runtime-session-key>, returns one session plus recent agent_runs, recent agent_events, resolved app chat transcript rows from chat_nachrichten, raw bootstrap_meta, raw state_json, optional external-session metadata from agent_external_sessions, shared-memory stats, lossless-context stats, normalized promptBudgetStats, normalized compactionStats, normalized memoryFlushStats, and the current safeguard strategy snapshot used by the Sessions inspector
    • the Sessions UI now shows both the native session runtimePath and the resolved execution harness/bridge from externalSession metadata, including the preferred external harness session id when a Codex or Claude subscription bridge is attached
    • the Agent Orchestrator web module also consumes this endpoint to surface Clapilot-code (embedded_pi) and Claude bridge sessions inside its activity rail. Claude bridge details use the same turn-card timeline format as interactive Codex output and include a footer composer for same-session follow-ups; Clapilot-code entries retain same-session follow-ups through the native runtime path.
    • promptBudgetStats includes model-limit provenance so the inspector can distinguish explicit provider limits, shipped model fallbacks, and the generic emergency fallback
    • compactionStats / memoryFlushStats include the current maintenance owner metadata for the native session layer
  • POST /api/agent-runtime/sessions?sessionKey=...
    • admin-only runtime-session follow-up endpoint used by the Agent Orchestrator module for Claude CLI bridge sessions
    • accepts { message?, attachments?, model?, preferSteer?, steerOnly? }; attachments[] use the same base64/data-url shape as orchestrator session attachments
    • with preferSteer/steerOnly, forwards first to POST /internal/runs/steer so active Claude CLI bridge streams can receive the prompt through stdin; otherwise it starts a normal native runtime turn with stored history enabled, which resumes the persisted Claude bridge session for the same sessionKey
  • GET /api/media-generation/providers
    • admin-only settings endpoint for media-generation providers and the curated catalog. Returns redacted Gemini video/music and Kie.ai video/music configs including capability, enabled flag, base URL, model list, live-discovered available_models, default model, settings JSON, and API-key hint. Gemini media uses the configured Gemini Runtime provider key instead of a second media-specific key. Kie.ai video model options include Veo 3 presets plus kling-3.0/video, bytedance/seedance-2, bytedance/seedance-2-fast, and bytedance/seedance-2-5.
  • POST /api/media-generation/providers
    • admin-only update endpoint for those provider configs and optional catalog. API keys are encrypted before storage; leaving api_key empty preserves the stored key unless clear_api_key=true. Catalog entries must reference enabled compatible providers, duplicate provider/model pairs are removed, and exactly one default is assigned per non-empty capability list.
  • POST /api/internal/livestream/topup
    • internal-only endpoint called by clapilot-streamer with x-clapilot-agent-secret plus Authorization: Bearer <internal-secret>. It wakes ClapilotAICore with queue/buffer context when the stream's agent top-up loop is due and logs agent.topup.* events.
  • POST /api/internal/livestream/media-generation/poll
    • internal-only endpoint called by clapilot-streamer with x-clapilot-agent-secret plus Authorization: Bearer <internal-secret>. It polls pending Kie.ai livestream media jobs by stored task ID, downloads completed provider result files into the shared media output directory, updates asset status/provenance, and records completion/failure events.
  • POST /api/internal/livestream/youtube-chat/poll
    • internal-only endpoint called by clapilot-streamer with x-clapilot-agent-secret plus Authorization: Bearer <internal-secret>. When YouTube chat ingest is enabled, it uses the linked Google OAuth user with YouTube readonly scope to discover the active broadcast, poll liveChatMessages, persist deduplicated chat messages, classify viewer questions/music/video/topic wishes into audience requests, and update chat poll health on the livestream channel.
  • POST /api/agent-runtime/tool-proxy
    • personal calendar_* tools require a resolved human user. Userless team/service automations return tool output { ok: false, code: "CLAPILOT_CALENDAR_USER_SCOPE_REQUIRED", message } (localized) before calendar queries or action-approval processing. User-linked calendar deletion still requires the normal approval.
    • unexpected execution failures return localized CLAPILOT_TOOL_EXECUTION_FAILED output with optional cause: { name, code } for driver/SQLSTATE identifiers. Raw error messages remain in server logs alongside toolName, runId, and sessionKey.
    • telephone clapilot_delegate requests require sessionKey: call-agent:<UUID> and arguments.instruction (optional arguments.context); execution uses the persisted active call owner/service principal and per-call tool permissions. Delegation runs use channel:call-agent-delegate:<UUID>, with permissions checked again for every proxied tool and hang-up bound to that call.
    • internal-only bridge for Clapilot-owned tool delegation
    • authentication accepts either the configured global internal secret or a random run-scoped execution capability issued to an active queued, running, or cancelling run. The run-scoped capability is sent in x-clapilot-agent-secret and requires a body sessionKey; when originSessionKey is present, the capability is bound to that originating session so same-identity delegated calls can still target another session. Any supplied userId, servicePrincipalId, or servicePrincipalSlug must match the persisted originating-session identity. Malformed, expired, cross-session, or identity-mismatched capabilities return 401. Shell children receive only the scoped capability, never the global internal secret.
    • session scope is resolved from persisted runtime state, not from a group:-looking key. Team execution requires a live room plus the persisted global_team_service principal; actor_user_id retains the verified triggering room member only for personal integrations and audit fields. Persisted active service-principal sessions are also valid without a Team Chat room for userless system automations such as Morning Briefing TTS; this does not grant Team Chat identity, and caller-asserted global principals without persisted session binding remain rejected. originSessionKey binds CLI/MCP/rescue calls to their originating identity: Team Chat delegation must retain room, principal, and actor, while system automation delegation must retain the same persisted service-principal ID.
    • proxies Clapilot UI tools plus native agent_todo_update, exec_command, read_file, edit_lines, write_file, web_search, context_search, context_get, memory_search, memory_get, memory_grep, memory_describe, memory_expand, knowledge_search, knowledge_get_entity, knowledge_neighbors, knowledge_explain_claim, learning_search, learning_get_object, session_status, package_install, issue_reporter_create, calendar_*, aufgaben_*, scheduled_tasks_*, specialized_agents_*, notizen_* including notizen_duplicate_local, documents_*, google_drive_list_files, google_drive_browse, google_drive_import_file, mandanten_*, cases_*, excel_*, word_*, google_meet, livestream_*, x_create_post, and x_get_bookmarks into the correct backend path; direct MCP calls and tool_execute use the same native proxy registry; native provider loops and MCP bridges can discover these concrete tools through tool_catalog_search and execute bridge-discovered tools through tool_execute without exposing the entire concrete catalog as model-facing functions up front; google_drive_list_files uses the dedicated Agent Google OAuth connection directly and never requires browser login; web_search uses the ClapilotAICore Search Providers setting and returns result titles, URLs, snippets, provider, and fallback attempts; context_search is the preferred unified read-only retrieval path across Learning, Wiki, native memory, exact session history/summaries, and the Knowledge Graph, while context_get reads one source-qualified hit; learning_search and learning_get_object are read-only and return only approved, visible, prompt-eligible Learning objects; x_create_post publishes through the current user's connected X/Twitter OAuth account, validates the requested post shape against the exact required X scopes before calling the API, and returns tweet_id plus url only after the X API confirms creation; x_get_bookmarks reads the connected account's bookmarked posts (max_results, pagination_token) and requires the bookmark.read scope, returning a structured missing_scopes reconnect hint when it was not granted; clapilot_context_status returns the active ClapilotAICore mediaDefaults plus mediaDefaultsGuidance so agents treat configured TTS as a Clapilot runtime route rather than inspecting raw provider secret fields; google_meet supports setup_status, join, status, speak, start_transcription, transcript, audio_transcript, start_voice, voice_status, stop_voice, stop_transcription, create_summary_document, and leave for managed browser-participant Google Meet sessions, with join automatically requesting captions and starting the server-side Realtime voice-to-voice bridge when admitted and enabled in Live Voice settings, audio_transcript recording/transcribing short incoming-audio fallback blocks, speak using configured chat TTS to play one-shot audio into the Meet microphone stream, and transcription before writing summary documents into Clapilot; livestream_* covers Live Stream Studio status, YouTube chat audience requests, clip briefs, local HTML/SVG video rendering with runtime-default TTS, rendered asset registration, approval, queueing, RTMPS start/stop desired-state changes, agent top-up configuration, and media-generation provider job submission. livestream_render_html_video.audio_path accepts only an owner-matched Agent Media file returned by media_tts_speak; when omitted, the renderer invokes the same configured runtime-default TTS path itself. It never invokes HyperFrames TTS or espeak-ng, fails visibly on TTS errors, and normalizes final audio to AAC 48 kHz stereo; specialized_agents_list, specialized_agents_get, specialized_agents_create, and specialized_agents_update expose admin-only, main-agent-only specialist catalog management without accepting channel-token secrets; scheduled_tasks_create and scheduled_tasks_update now accept optional specialized_agent_id in addition to plain model pinning, optional notify_result_mode for result delivery, and trigger_kind="webhook" for token-backed inbound triggers; specialist-bound automations inherit the selected agent's default model instead of exposing a second execution-model override
    • returns top-level ok in sync with the nested JSON tool output, so ClapilotAICore treats normalized HTTP-200 tool failures as failures; handled validation failures retain code="tool_error" unless the tool supplies a more specific code; successful aufgaben_* mutations on shared boards and shared-board creation fan out one audience-scoped UI mutation row visible to every user's live stream for both user-bound and privileged userless sessions, while private-board mutations remain scoped to the owning user; shared-to-private moves use an owner-excluded audience removal plus the full update on the owner's stream; Team Chat tasks retain source_type="team_chat" and an origin-room source ID decoded by web and Apple clients
    • documents_get now enriches reads with Word Editor text for supported writing files when available and otherwise falls back to document-analysis preview text instead of metadata-only responses
    • fail-closed: requires a valid global internal secret or active run-scoped capability in x-clapilot-agent-secret; requests without either credential are rejected even when no global secret is configured
  • /api/livestream/studio
    • GET: admin-only Live Stream Studio summary with the default YouTube channel config, redacted stream-key state, queued clips, assets, scanned video files from the shared livestream media directory, recent FFmpeg runs, recent events, YouTube chat messages, detected audience requests, queue buffer seconds, approved buffer seconds, and current asset.
    • POST: admin-only action endpoint. Supports update_channel, set_agent_direction, run_agent_topup, create_asset, update_asset, enqueue_asset, enqueue_media_file, delete_media_file, reorder_queue, update_queue_item, set_stream_state, update_youtube_chat, and poll_youtube_chat. Stream keys are encrypted before storage; update_channel also persists agent_topup_* controls; update_youtube_chat links the current admin's Google OAuth connection for YouTube chat ingest; set_agent_direction stores the natural-language stream briefing and can immediately wake the livestream agent; file queueing/deletion is restricted to videos inside the shared livestream media directory; delete_media_file blocks active pending/playing queue references and archives linked assets after removing the file; asset queueing requires an approved asset with a rendered media_path; update_queue_item can set loop_count so a queue row stays in the loop and appears that many times per playlist cycle.
  • /api/livestream/availability
    • GET: authenticated lightweight reachability endpoint for the sidebar. Returns the livestream streamer status, last heartbeat, heartbeat age, stale threshold, reason, and available=true only when clapilot-streamer has written a fresh non-offline heartbeat.
  • /api/livestream/media
    • GET: admin-only inline video preview endpoint for files inside the shared livestream media directory. Accepts path, validates that it resolves inside the media directory, and supports byte ranges so the Studio's video-library tiles can use native browser playback controls without exposing arbitrary filesystem reads.
  • /api/clapilotaicore/live-voice-settings
    • GET: authenticated Realtime and Live Voice settings payload for the ClapilotAICore Audio settings page, including the general Realtime provider/model routing dropdown options and provider readiness (enabled, API-key presence, and Live Transcribe readiness), api_live_transcribe_enabled, api_live_transcribe_model, compatible live-transcription model options, Google Meet enablement, optional Meet-specific Realtime model override, Meet-specific voice, and default Meet participant display name
    • POST: admin-only update endpoint for those Live Voice settings; API Live Transcribe can only be enabled when the selected Realtime provider is enabled, is an OpenAI API-key provider with a stored key, and the model is compatible with live browser transcription. Text-model preflight health does not gate audio-only use. Invalid configuration returns a localized 400 response instead of persisting an unusable gate. The general Realtime provider/model pair is persisted in app_settings.native_model_routing; API transcription values use app_settings.api_live_transcribe_enabled and api_live_transcribe_model; Google Meet values use app_settings.google_meet_live_voice_enabled, google_meet_live_model, google_meet_live_voice, and google_meet_agent_display_name
  • /api/chat/transcription/realtime/session
    • POST: authenticated two-minute OpenAI Realtime transcription client-secret creation. Disabled, missing-key, wrong-auth-mode, or administrator-disabled provider configuration returns localized 409 realtime_transcription_configuration_error; upstream OpenAI failures retain the upstream status, while unexpected session-creation failures return 502.
  • /api/clapilotaicore/dgx-telemetry-settings
    • GET: admin-only load of the DGX telemetry endpoint settings for Settings -> ClapilotAICore -> DGX Cluster; returns { api_base_url, effective_api_base_url, default_api_base_url, access_token_configured, can_edit } (the default is Clapilot Cluster Studio, http://192.168.178.39:8010; the password itself is never returned)
    • POST: admin-only update of app_settings.dgx_telemetry_api_base_url; accepts { api_base_url, access_token? }, allows an empty URL for the default endpoint, and validates HTTP/HTTPS URLs. A non-empty access_token replaces the shared Cluster Studio password (app_settings.cluster_studio_access_token, encrypted, also used by model training); omitting it keeps the stored one
  • Every upstream call below goes to Cluster Studio /api/fleet/* with Authorization: Bearer <Cluster Studio password>. Upstream 401/403 map to 502 { error: "upstream_unauthorized" }; other failures map to 502 { error: "upstream_unreachable" }. Upstream auth failures are never passed through as 401, which clients treat as an expired Clapilot session
  • /api/clapilotaicore/dgx-telemetry/stats
    • GET: admin-only proxy to /api/fleet/stats; returns the JSON snapshot as-is, currently schema v4 { schema_version, training_url, media, training, machines: [...], clusters: [{ cluster_id, cluster_name, up, served_model, nodes, nodes_online, nodes_total, static, hist, ...metrics }] }; the web and Apple consumers also accept the schema v1 flat snapshot
  • /api/clapilotaicore/dgx-telemetry/health
    • GET: admin-only proxy to /api/fleet/health; passes through { ok, schema_version, clusters: [{ cluster_id, up, model, endpoint }] } with status 200 when any cluster is serving or 503 when none are serving
    • POST: admin-only test-connection proxy for an unsaved { api_base_url, access_token? } draft; a draft access_token is used instead of the stored password for this request only; passes through the same upstream JSON and 200/503 status
  • /api/clapilotaicore/dgx-telemetry/schema
    • GET: admin-only proxy to /api/fleet/schema
  • /api/clapilotaicore/dgx-telemetry/stream
    • GET: admin-only SSE pass-through proxy to /api/fleet/stream; returns text/event-stream and lets the browser reconnect manually when the upstream is unavailable

Automation webhook durability

GET|POST|PUT|PATCH /api/automation-webhooks/{token} returns run_id only after the corresponding agent_runs reservation and webhook payload queue row commit atomically. Repeated provider delivery IDs return the original run. Runtime termination leaves auditable terminal fields and the queue retries the payload; result delivery is idempotent by run_id.

Video Studio voiceover

GET /api/video-studio/voiceover?slug=<project> returns the persisted voice, provider, scene timings, editable texts, and generated segment metadata for an admin-owned Video Studio project. POST /api/video-studio/voiceover accepts action=generate-all, generate-segment, or remix, together with slug, provider (openai, gemini, or openai_compatible), voice, optional owned-character UUID voice_character_id, and the scene segments. When voice_character_id is set, the character must have an uploaded sample and the provider must be openai_compatible; the sample is supplied to every synthesized segment as the voice-clone reference. Generation resolves credentials through Clapilot's configured server-side TTS runtime. The remix action performs audio-only FFmpeg muxing; refreshBase=true snapshots a newly visual-rendered MP4 first.

Model training and evaluation

All routes below require an administrator session and run in the Node.js runtime. Upstream network/timeouts return 502 { "error": "upstream_unreachable", "detail": "..." }. Other upstream failures return upstream_error; a one-run-at-a-time conflict from job creation returns 409 { "error": "training_already_running", "detail": "..." }.

MethodRouteRequestResponse
GET/api/clapilotaicore/model-training/settings-{api_base_url,effective_api_base_url,default_api_base_url,access_token_configured,can_edit}
PUT/api/clapilotaicore/model-training/settings{api_base_url,access_token?}; empty URL means the default; a non-empty access_token replaces the shared Cluster Studio password, omitting it keeps the stored oneSame settings payload
GET/api/clapilotaicore/model-training/stats-{up,ts,node:{ram_total_gb,ram_avail_gb,ram_used_gb,gpu_util,gpu_temp,gpu_watts},docker,active,total_runs,completed,failed,current,last}
GET/api/clapilotaicore/model-training/runs-TrainingRun[], including history: [[step,loss]], progress, ETA, state, paths, and hyperparameters
GET/api/clapilotaicore/model-training/jobs-FineTuneJob[] (the upstream list wrapper is normalized away)
POST/api/clapilotaicore/model-training/jobs{training_file,suffix?,hyperparameters:{targets?,rank?,alpha?,learning_rate?,seq_len?,batch_size?,grad_accum?,max_steps?,epochs?}}FineTuneJob
GET/api/clapilotaicore/model-training/jobs/:jobId-FineTuneJob
GET/api/clapilotaicore/model-training/jobs/:jobId/events-FineTuneJobEvent[]
POST/api/clapilotaicore/model-training/jobs/:jobId/cancel-Cancelled/cancelling FineTuneJob
POST/api/clapilotaicore/model-training/datasets/uploadMultipart file{id,lines}
POST/api/clapilotaicore/model-training/datasets/from-chat-export`{instances?:string[],sources?:('personal_chat''group_chat'
GET/api/clapilotaicore/model-training/compare/models-{eval:{up,running,models,base,adapters},gateway}
POST/api/clapilotaicore/model-training/compare/start-Upstream start result
POST/api/clapilotaicore/model-training/compare/stop-Upstream stop result
POST/api/clapilotaicore/model-training/compare/load{run_id}{loaded}
POST/api/clapilotaicore/model-training/compare/chat{model,messages,max_tokens?,temperature?,seed?}{model,content,seconds,tok_per_s} plus upstream metadata
POST/api/clapilotaicore/model-training/compare/verify{run_id?:string,model?:string,prompt?,max_tokens?}{base:{content,seconds,tok_per_s},adapter:{...},identical}
GET/api/clapilotaicore/model-training/streamSSEEvery three seconds: data: {stats,runs}; upstream failures use an SSE error event

The verify route resolves ft:<run_id> and the current base model from the models endpoint. Both requests use the same prompt, temperature 0, and fixed seed. identical: true is an inert-adapter warning, not success.

Testphase double opt-in

  • POST /api/public/testphase-signup requires X-Clapilot-Embed-Key, an allowed request origin, an accepted consent value (true, "true", "on", or 1), JSON contact fields (name or firstName/lastName, email, optional company, industry, phone, bottleneck/notes, language, consentVersion), the deployment CAPTCHA token when configured, and an empty website honeypot. A separate attempt ledger atomically rate-limits every recipient and additionally limits trusted client IPs. The first request creates the recipient's pending lead and sends its confirmation email; repeats preserve the existing fields and token and do not resend. SMTP or persistence failures return a non-success JSON response and remain visible in the lead delivery state.
  • GET /api/testphase-leads?page=1 returns 50 workspace-global leads plus pagination metadata (page, pageSize, and total). format=csv exports at most 10,000 recent leads with Excel-safe UTF-8 CSV encoding. Authenticated operators can POST { id, action }, where action is retry_confirmation for a pending failed confirmation or retry_internal for a confirmed failed internal notification.
  • GET /api/public/testphase-confirm?token=... renders a no-cache confirmation interstitial. Its explicit POST consumes the seven-day token once, records confirmation time and a validated trusted-proxy IP, sends the internal notification only for a confirmed lead, and redirects to https://clapilot.com/testphase/bestaetigt/ with a result status.

Remote specialist sessions

Specialized-agent create/update accepts executionConfig (or execution_config) containing {target: "instance" | "remote", runnerId: string, computerUse: boolean, computerProvider: "native" | "cua", cuaSandbox: string, browser: boolean, webSearch: boolean}. Responses and the overview return execution_config. Default target is instance. Remote execution requires a Codex model and a runner ID. computerProvider: "cua" (the only provider on the instance) requires an existing Cua sandbox name in cuaSandbox; "native" uses the remote runner's Codex Computer Use plugin.

EndpointAuthenticationContract
GET /api/specialized-agents/remote-runnersAdministrator{runners: [{id, label, capabilities: ["codex", "computer", "browser"], lastSeenAt, stale}]}. Plugin capabilities do not guarantee a connected Chrome backend.
GET /api/specialized-agents/remote-requestsSigned-in user{requests: [{id, runId, agentId, runnerId, method, params}]} for that user's active runs only.
POST /api/specialized-agents/remote-requestsSame initiating user{runId, id, decision?, answers?}; returns {ok: true}. Decisions and answers follow the native request type.
POST /api/modules/agent-orchestrator/api/remote-runners/specialist/pollFleet machine HMAC{runnerId, label, capabilities, busy, active:[{runId,claimToken}]} returns {assignment, claimToken?, cancel, responses}.
POST …/specialist/eventSame HMAC and active claim{runnerId, runId, claimToken, sequence, event}; ordered, retry-safe thread/delta/tool/request/completed/failed events.
POST …/specialist/toolSame HMAC and active claim{runnerId, runId, claimToken, callId, name, arguments}; scoped MCP tool result. Identical active call IDs return the same result; changed arguments are rejected.

The machine identity must match runnerId. Specialist routes authenticate before dispatch and do not depend on module installation. Internal equivalents are under /internal/remote-specialists/, protected by the native runtime token. Claims use a 30-second central lease; runners stop execution after 20 seconds without contact. Session bindings are durable; pending prompts and deduplication live for the active runtime attempt, and interrupted attempts are not automatically replayed.

specialized_agents_list also returns remote_runners with machine IDs and capabilities for administrative agent setup (null when the native runtime is unavailable). Specialist tool responses include execution_config. Native MCP tool-call approvals with empty form schemas can be explicitly accepted; other MCP elicitation forms remain decline-only.

Inline remote-agent approvals

Remote specialist approvals and questions appear as cards in the originating conversation on web Chat, Team Chat, the floating chat panel, and the iOS/macOS chat transcripts. They never open a global modal or sheet. Each card provides the native request text, collapsible technical details, and the supported Allow/Decline or answer controls. A successful decision remains visible in that open transcript; the runtime audit remains authoritative after navigation or reload.

GET /api/specialized-agents/remote-requests?sessionId=<chat-session-or-team-room-id> returns only pending requests owned by the authenticated initiating user and matching that conversation. Pending request objects include sessionKey; matching ignores the specialist handle but compares the complete session identity. Omitting sessionId preserves the owner-scoped listing for existing clients. POST request and approval semantics are unchanged. Unsupported native elicitation still requires interaction on the runner; moving the card does not grant macOS permissions or bypass approval.

Agent Orchestrator repository configuration retention

The existing Agent Orchestrator config repoConfigs array retains valid repository entries even when no task board or automation is enabled. Status returns those saved entries so settings can render them without loading the repository catalog. Repository discovery remains separate and does not implicitly configure repositories. Request and response field shapes are unchanged.

Agent Orchestrator per-machine capacity

  • GET /api/modules/agent-orchestrator/api/remote-runners: each runner adds integer orchestratorSlots (default 1, range 0–8), orchestratorAvailableSlots, orchestratorSlotsSupported, and slotLimitsLoaded. Availability excludes stale/older/incompatible runners, occupied jobs, and synchronous reservations. Clients disable slot mutation and surface an operator hint when the runner contract is unsupported or the persisted limits have not loaded.
  • The same listing also returns per runner hostHealth (the last reported { freeDiskBytes, totalDiskBytes, freeMemBytes, totalMemBytes, dockerOk, checkedAt }, or null when the machine never reported it) and the derived boolean hostHealthy. Automation availability additionally excludes a runner whose reported free disk is below AGENT_ORCHESTRATOR_RUNNER_MIN_FREE_DISK_BYTES (default 10 GiB); unknown or unreported health never excludes a runner.
  • POST /api/modules/agent-orchestrator/api/remote-runners/{id}/orchestrator-slots: admin module access; body { "slots": 0..8 }; returns { "runner": ... }. Invalid or fractional limits return 400; unknown runner returns 404. Limits persist by runner ID in agent_orchestrator_runner_limits. Running jobs finish when limits decrease.
  • Orchestrator status capacity.availableSlots includes local and remote capacity; localAvailableSlots and remoteAvailableSlots split it. maxConcurrentAgents/effectiveMax now apply to local automation harnesses.
  • Runner heartbeat/claim control adds maxConcurrent for the effective machine capacity. Runner 0.3.1+ handles an optional assignment checkout object (sha, branch, upstream, refspecs) to reproduce the prepared automation checkout. New automatic overflow is withheld from older runners. Existing manual assignment shapes remain valid.

Fleet overflow reliability

Changing per-runner Orchestrator slots requires an administrator session on the module HTTP endpoint; authenticated internal service calls retain the existing trusted-service access. Agent tools retain their administrator check. Before the first successful database load, unknown runner slot limits allow no automation; after a successful load, transient failures preserve the last known limits. Manual base capacity remains available.

Runner control payloads and listings expose base capacity plus Orchestrator slots (maximum 16 on runner 0.3.2+; 0.3.1 runners retain automation capped at 8 until their idle-time self-update; unsupported native runners retain only their reported base capacity while their saved Orchestrator slot setting remains visible). The bundled runner does not advertise a complete model catalog: absent model restrictions mean the selected model is attempted by the configured harness, and an unsupported-model error follows normal job failure handling. Optional advertised model maps constrain routing; an explicit null clears previous restrictions. Models are never silently substituted.

Agent Orchestrator Codex Cloud

GET /api/modules/agent-orchestrator/api/status includes cloud configuration in repoConfigs: codexCloudEnabled (default false), codexCloudEnvironmentId, and codexCloudBranch. POST .../config preserves these fields when saving repoConfigs; cloudRepository: { repo, integrationName, codexCloudEnabled, codexCloudEnvironmentId, codexCloudBranch } patches only the cloud fields of one existing GitHub repository. Enabled entries require a valid environment identifier and branch. There is no migration: configuration uses the existing repository JSON setting.

POST .../jobs supports { task, repo, forgeProvider: "github", forgeIntegrationName, provider: "codex", executionTarget: "cloud" }. The configured repository environment and branch are authoritative; clients cannot override them per job. The response retains { jobId, status, streamUrl }. GET .../jobs/:id additionally returns cloud: { environmentId, branch, taskId, url, submissionStartedAt, lastCheckedAt, status, statusText, trackingError, resultText, diff, diffError } as available. GET .../jobs includes cloud metadata but omits diff and resultText. Only retrieval and event-stream operations are supported on cloud jobs; local lifecycle/workspace mutations return HTTP 409 with code: "codex_cloud_manage_externally" and cloudUrl.

The host submits through bounded, shell-free codex cloud exec subprocesses. Manual and automatic polling use the bounded read-only task-result adapter to retrieve status, report and diff together. HTTP 401 permits one read-only CLI credential refresh before retry; other polling failures remain observable and never resubmit the task. Cloud task IDs survive restarts; ambiguous submission failures require inspecting Codex Cloud before manually retrying. A successful submission is not proof of task completion.

File context-menu mutations

Method and pathBodyResult
POST /api/documents/:id/copy{}New document, preserving folder, metadata and visibility with independent file bytes
POST /api/modules/file-explorer/api/copy{ path }{ ok, path }; allocates an unused sibling filename, never overwrites
POST /api/modules/video-studio/api/video-copy{ name }{ ok, name }; independent rendered video bytes
POST /api/modules/video-studio/api/video-rename{ name, title }{ ok, name, title }; updates display title and preserves project/version file paths
POST /api/video-studio/ai/projects/:id/copy{}{ id }; independent storyboard, no generation started

All endpoints require the corresponding authenticated write access. File Manager retains real-path containment and protected runtime-directory checks. Video names must be basenames and remain within the studio video directory. Document copies respect shared/private access. Storyboard copies are workspace-global, consistent with Video Studio. Existing update/delete, export, public document-share, email draft, Fax, Post, and Team Chat routes power the remaining menu actions.

/file-actions is an authenticated Apple-client handoff for document sharing and downloading, not a public file endpoint. Native handoffs identify the exact record with fileActionId; Canvas additionally preserves fileActionShared.

Authenticated file deep links

Original itemWeb destination
Document/dokumente?preview=<id>
Canvas/modules/canvas?file=<path>&shared=1 or shared=0&owner=<user-id>
Note/modules/notizen?noteId=<id>
Whiteboard/modules/whiteboard?board=<id>
File Manager/modules/file-explorer?path=<parent>&file=<absolute-path>
Rendered video/modules/video-studio?video=<filename>
Video storyboard/modules/video-studio?aiView=board&aiProject=<id>
Word / Excel/modules/word-canvas?docId=<id> / /modules/excel-canvas?docId=<id>

All query values must be URL-encoded. Internal channel/DM shares put an absolute same-instance URL into the draft, retaining the original item and permissions. No document export, upload, or public-share endpoint is called. Missing or unauthorized targets do not silently select a different file. Canvas explicitly fetches linked files beyond list pagination and honors shared/private scope. GET /api/modules/canvas/api/files/<path> accepts owner for private links and returns 404 when it differs from the authenticated user; this never grants access. Apple universal links and in-chat links preserve the full file query and open the authenticated item in-app, including File Manager and Excel module surfaces.

Internal file preview metadata

GET /api/file-references/resolve?href=<URL-encoded-local-deep-link> requires an existing authenticated user (or modules:api:read principal). It returns { href, title, kind: "reference" | "image" | "video" | "audio", mediaUrl?, documentId?, mimeType?, fileName? }. The canonical href opens the original item; mediaUrl is an authenticated media endpoint, never a public-share token.

Only recognized local routes are accepted. Arbitrary hosts and external URLs are rejected (400), unauthenticated requests return 401, and inaccessible/missing items return 404 without metadata. Private Canvas links require their original owner scope. Existing feature APIs enforce document/module permissions. Internal metadata requests use trusted server addresses, never request-provided hosts. Responses are private/no-store. No export, attachment creation or permission mutation occurs. The client keeps a reference fallback if resolution fails.

Image model catalog for module pickers

GET /api/generated-images/models requires an authenticated user and returns { entries: [{ providerSlug, model, isDefault }] }. Entries mirror the Settings image catalog; an empty catalog returns shipped image choices with empty provider slugs. No credentials are exposed. Responses are not cached. Whiteboard forwards the selected provider_slug and model to image generate/edit endpoints; Standard omits overrides.

Codex Cloud environment picker

GET /api/modules/agent-orchestrator/api/cloud-environments?repo=owner%2Frepo returns { repo, environments: [{ id, label }], setupUrl, canCreate: false }. Only GitHub repositories are accepted. Authenticated module access follows the existing Orchestrator boundary; no credentials, setup scripts, secrets or task content are returned. This read-only lookup uses the host's Codex OAuth credentials and the repository-specific endpoint used by the official Codex CLI picker. Responses and duration are bounded, redirects are disallowed, and HTTP 401 can trigger one CLI credential refresh before retrying. Invalid repositories return 400; account, network and schema failures return 502 codex_cloud_discovery, distinct from a successful empty result. Creation remains in Codex's environment settings; no private write contract is used. Web, iOS and macOS select a single match in the configuration draft and preserve existing selections during errors.

POST /api/chat/sessions/{id}/close

Authenticated personal-session lifecycle request; no body. Returns 202 { "queued": true } after PostgreSQL records the request, 401 without authentication, or 404 for an inaccessible session. ClapilotAICore processes the request asynchronously after active runs finish, summarizing history and extracting personal memory under existing policy. It preserves the chat session and messages. This endpoint is safe to repeat and is not a deletion or reset operation. Requires migration 307_chat_session_close_jobs.sql and the updated native runtime.

GET /api/chat/sessions/activity

Accepts repeated id query parameters for up to 20 personal chat sessions. Requires authentication and filters sessions by the current user. Returns { "sessions": [{ "id": "...", "status": "idle|working|complete|error|stopped" }] }; inaccessible IDs are omitted. Supplies the workspace-tab working/completion indicators without exposing transcript content or mutating sessions. Active/queued/cancelling native runs and pending replies take precedence over older completed responses.

Native Image Playground client

The Apple editor consumes existing endpoints without changing request or response shapes: GET /api/generated-images/models, POST /api/generated-images/generate, POST /api/generated-images/edit, multipart POST /api/generated-images/import, authenticated GET /api/generated-images/:id, and GET/PUT /api/modules/image-playground/api/state. State remains personal and compatible with the bundled web module. Pencil/pointer selections become PNG masks with transparent editable regions, imported with image-playground-mask and passed as mask_image_id; generation references use reference_image_ids.

Repository automation execution priority

Agent Orchestrator repository configuration includes nullable executionPriority: ("local" | "remote" | "cloud")[]. A saved array must contain each target exactly once; null or absence preserves legacy local-first/Fleet-overflow behavior. Both the full repoConfigs configuration and POST /api/modules/agent-orchestrator/api/config with { "executionRepository": { "repo": "owner/name", "forgeProvider": "github", "integrationName": "", "executionPriority": ["remote", "cloud", "local"] } } support updates. The targeted patch preserves other repository fields and requires an existing exact repository/integration match.

Cloud eligibility remains Codex-only and requires enabled GitHub Cloud configuration. Automatic Cloud jobs may include cloud.requiresReview, cloud.submissionUncertain, cloud.interrupted, and cloud.publishedPrUrl. An unresolved attempt holds the Cloud target; independent Claude/local/Fleet workflows remain eligible. Pre-application failures persist cloud.fencedAt and cloud.fallback: { attemptedAt, reason, status, executionTarget? } before trying compatible unattempted targets in priority order. Known terminal failures use a five-minute cloud.retryAfter cooldown. cloud.applicationStartedAt and cloud.workflowKey prevent replay of a partially applied workflow. cloudAvailableSlots is separate from local and Fleet capacity. Active automatic Cloud tracking jobs reject DELETE with HTTP 409; removing a failed tracking job after reviewing its Cloud result releases the hold. Automatic Cloud jobs interrupted by a restart become failed. Attempts without an application checkpoint are reconciled read-only by task ID: terminal reports/diffs are retained, fenced against application, and Cloud capacity is released with cloud.reconciledAt recorded. Unknown submissions, partial applications and legacy ambiguous result checkpoints remain held for inspection. Manual Cloud jobs retain status polling after restart.

Immobilien and optional ImmoScout24 preparation

Authenticated GET /api/real-estate?q=&offset=0&archived=false returns {properties,has_more} (100 per page). GET /api/real-estate?id=<uuid> returns {property,records}. Properties and records contain id, data, revision, updated_at; records additionally have property_id. Data is workspace-global.

POST /api/real-estate accepts {action,id?,property_id?,expected_revision?,data}. Actions: property_create, property_update, record_create, record_update. Update data replaces the entire structured record. Properties require number,title and support street,postcode,city,kind,market,stage,price,area,rooms,owner,description,next_step. Record data: kind,status,title,notes,contact,starts_at,ends_at,reference. Viewing timestamps require an explicit timezone and end after start. References are relative links to existing Clapilot document/customer/task/calendar surfaces. Success: {item}; invalid input 400, missing parent 404, stale revision or duplicate number 409, unexpected failure 500. Errors include a localized error; estate errors also expose code. Pass ui_language=de|en|it.

Administrator-only GET /api/app-connections/immoscout24 returns {configurations:[{environment,revision,updated_at}],connected:false,publish_enabled:false}. POST accepts {action:"save"|"remove",environment:"sandbox"|"production",consumer_key,consumer_secret,expected_revision} (revision 0 for a new environment). Blank credentials retain current values. Secrets are never returned. Conflicts return 409. The endpoint does not establish OAuth authorization or make outbound provider calls. Removing configuration does not delete local properties or remote listings.

The exact /api/real-estate path is registered for agent-system authentication in middleware (modules:api:read for GET, modules:api:write for mutations). The route resolves the authenticated user or verified scoped service token; submitted creator IDs cannot replace that actor. Mutation responses additionally return href: /modules/immobilien?property=<uuid>; web deep links select the requested property. App-connection configuration is deliberately excluded from this agent-token route allowance.

Property gallery

GET /api/real-estate?images_for=<property UUID> returns {images:[...]}; GET /api/real-estate?id=<UUID> also includes images alongside property and records. Metadata fields: id, property_id, filename, caption, source_url, width, height, is_cover, revision. GET /api/real-estate?image_id=<image UUID> serves authenticated image/webp, with Cache-Control: private, no-store; removed/missing images return 404.

POST /api/real-estate accepts image_add (property_id, data:{image_base64,filename?,caption?,source_url?}), image_import (property_id, data:{source_url,filename?,caption?}), image_cover or image_remove (property_id, id, expected_revision). Responses use the existing {item,href} shape. Input limit: 5 MB decoded JPEG/PNG/WebP, 32 MP, 40 active images/property; JSON bodies are bounded to 7.2 MB before parsing. Remote sources must be direct public HTTPS image URLs on port 443 without credentials/redirects. Errors use localized image, conflict, missing, invalid, or failed codes; stale revisions return 409. Migration 309_real_estate_images.sql is required before deployment. Images are workspace-global and audited; removal retains the underlying bytes.

Property list responses include nullable cover_image_id, selected from active gallery images (cover first). Web, iOS and macOS property cards display this authenticated image as a preview; gallery cover changes and removals update the card. Properties without images remain text-only. The real_estate_workspace list action returns the same metadata; no new action or image permissions are introduced.

Real estate detail GET responses additionally include location: {address,map_url,embed_url} | null, derived from street, postcode and city. No coordinates are stored or inferred as verified facts. Existing authentication applies; deriving links does not contact a map provider.

Native runtime health and WhatsApp status deadlines

GET /health on the native agent returns HTTP 200 when the shared database pool serves its probe, and HTTP 503 on failure. database.pool includes numeric total, idle, waiting, and max counts on either outcome; database.reason describes a failed probe. Pool acquisition defaults to 5000 ms (CLAPILOT_AGENT_DB_CONNECTION_TIMEOUT_MS, bounded to 100–30000 ms), and the health query has a 2000 ms query deadline. The existing app transport probe includes this response in probe.http.

GET /api/agent-runtime/channels/whatsapp/auth and the specialist equivalent /api/specialized-agents/{id}/channels/whatsapp/auth bound the runtime request, including response-body consumption, to 8000 ms. Timeout/body-read failure returns the existing HTTP 500 { error } shape instead of hanging or reporting empty success. QR actions through POST preserve their existing longer scan-wait budget. See the incident postmortem.

Speech catalog and provider voices

  • GET /api/speech/models (authenticated): {tts, stt, providers}. Model entries include providerSlug, providerLabel, providerType, model, isDefault, and optional voice. Provider candidates contain slug, label, ttsModel, sttModel, and optional ttsModels (suggested model IDs for the native settings picker; not enabled catalog entries). ElevenLabs suggestions include eleven_v4 and eleven_v4_turbo. Existing runtime defaults remain available before a catalog is configured.
  • POST /api/speech/models (admin): partial {tts?, stt?} arrays using the existing media catalog entry shape. Persists multiple provider/model choices and per-entry TTS voices in app_settings.media_model_catalog; default entries update native_model_routing.tts/stt. Other media capabilities are preserved. Legacy runtime settings saves preserve explicit catalog speech defaults. No migration is required.
  • GET /api/speech/voices?provider_slug=... (authenticated): {supported, voices:[{id,name}]}. ElevenLabs uses the selected provider's credentials and paginated v2 voice search. Compatible endpoints use /audio/voices; 404/405/501 mean discovery is unsupported. Other failures return a localized 502 error. Unsupported providers retain manual voice entry.
  • GET/POST /api/media-generation/providers catalog now also includes tts and stt arrays; omitted capabilities are preserved for older clients.
  • PATCH /api/video-studio/ai/scenes/[id] additionally accepts voiceover_speech: {providerSlug, model, voice} | null. It persists as metadata.voiceoverSpeech. POST .../dub synthesizes using that selection and the saved narration; no override uses the workspace default. Unavailable explicit provider/model selections fail instead of falling back to another provider. Preview/apply routes and their response shapes remain unchanged.

Agent Orchestrator coding-model priority

  • GET /api/modules/agent-orchestrator/api/coding-model-priority: returns {codingModelPriority: [{provider, model}]}. The authenticated workspace-wide list is also included in the Orchestrator status response.
  • POST /api/modules/agent-orchestrator/api/coding-model-priority: administrator/trusted-agent mutation accepting the same shape. Replaces the ordered list atomically, preserving other settings. Supports up to 30 unique pairs; provider is claude, codex, or clapilot-code; model is a nonempty ID, up to 240 characters. Duplicate provider/model entries (including runtime-prefixed aliases) and malformed values return 400. An empty array clears the configured chain.
  • Jobs expose modelAttempts for completed fallback transitions. The current selectedProvider and model identify the active attempt. Existing job logs carry each transition.

GET /api/modules/agent-orchestrator/api/coding-model-providers returns { providers: [{ slug, label }] } for enabled conversation providers supported by Clapilot-Code. Add ?slug=<provider-slug> to discover { providers, models: [{ value, label, provider }] }; model values are provider-qualified IDs and discovery does not add them to the global catalog. Priority entries retain { provider: "clapilot-code", model: "cursor-default/<discovered-model-id>" }. The native endpoint is GET /internal/coding-model-providers with the same query/response.

GET /api/modules/agent-orchestrator/api/coding-models merges the configured catalog with freshly loaded coding-priority entries for all harnesses and saved repository automation model pins. Response shape is unchanged; Clapilot-Code values preserve the full provider-slug/model-id, while Claude/Codex values use CLI model IDs. Removing a fallback entry does not remove an explicit repository pin.

Recent personal chats

GET /api/chat/sessions?recent=true requires a signed-in user and returns { sessions: [{ id, title, last_message_at }] } with at most three personal chat sessions, sorted by last_message_at DESC, created_at DESC, id ASC. Main/pinned status does not affect this order. This lightweight dashboard read does not consult the agent runtime or create a chat. Without recent=true, the session-picker response lists up to 200 sessions; each carries linked_channels (channel types such as whatsapp whose approved DMs write into that session, empty otherwise). The bundled recent_chats widget also exposes this caller-scoped data (plus href) through the installed-widget read APIs and agent tools.

Bundled dashboard counters (clients_summary, open_tasks, urgent_tasks, overdue_tasks) allow dashboard_h: 1 in PATCH /api/mini-apps/[id]/dashboard. The saved response retains this height; other widgets still require at least two rows at the API boundary.

Shared media library

Requires authenticated workspace access (session cookie or agent system token with modules:api:read / modules:api:write). No owner filter applies to library entries. Personal chat-only assets are not automatically registered.

  • GET /api/media-library?folder=&kind=image|video&search=&offset=0: indexes known module output and returns { assets, folders, total, offset, limit: 60 }. Assets contain id, name, mimeType, kind, folder, authenticated url, size, createdAt. An empty folder includes all folders; a named folder selects direct contents. Default folder identifiers are stable English paths; labels are localized in clients.
  • GET /api/media-library/:id: authenticated bytes with Range support (206/416); validates the real filesystem path inside the workspace. Never accepts a caller-supplied path or URL.
  • POST /api/media-library: { action: "create_folder", folder } creates a nested path; { action: "move_asset", id, folder } moves an asset reference into an existing folder. Both return { ok: true }. Traversal, empty path components, backslashes, and control characters are rejected.
  • POST /api/media-library: { action: "share_image", id } explicitly registers the acting user's generated image and returns { ok: true, id } (the library ID). Uploaded references/masks cannot be shared through this action.
  • POST /api/media-library: { action: "delete_asset", id } or { action: "delete_assets", ids: [...] } permanently deletes library entries together with the underlying module output for the whole workspace and returns { ok: true, deleted }. Generated images lose their generated_images row, workspace file, and the owner's Image Playground gallery version; Video Studio scene clips lose their generated_videos row and file, and referencing scenes fall back to frame_ready so they can be regenerated; rendered Video Studio videos lose the file and its title sidecar. Unknown ids and files outside the workspace are rejected (400), and a partially processed batch keeps the entries that were not reached.
  • DELETE /api/generated-images/:id: session-authenticated, owner-scoped hard delete of a generated image. Removes the generated_images row, the workspace file, and the shared media-library reference; returns { ok: true, id }, 404 when the image does not exist or belongs to another user. Used by the Image Playground gallery on web, iOS, and macOS for single and bulk deletes. Not exposed as an agent tool (parity with Video Studio video deletion); agents delete shared media through media_library_update.
  • POST /api/generated-images/generate and /edit: optional library_source: "image-playground" | "social-media" registers the result in the corresponding shared folder. Other callers retain existing visibility.
  • POST /api/modules/social-media/api/media/import: { kind: "media-library", asset_id } copies an allowed image/video into Social Media and returns the existing { item, media } contract. Existing MIME and upload-size limits apply.
  • POST /api/livestream/studio: { action: "import_library_asset", asset_id } is admin-only, accepts videos, atomically copies into the mounted stream media directory, and returns { file: { path, name } }. Does not enqueue or start streaming.

Migration: db/migrations/311_media_library.sql. The library uses PostgreSQL references and existing workspace files, without a new storage service or publicly accessible media URLs.

Storage cleanup merge retention

GET /api/admin/storage and instance_storage_status preserve the cleanup report’s per-worktree retention fields mergedImmediate and mergedGraceMinutes. Cleanup summaries add removedMergedCount, preserved.mergedGrace and per-removed-entry reason (merged or retention). Older reports default to age-only retention and zero counters. See Instance cleanup retention for detection, grace and safety semantics.

Symphony worktrees report mergedImmediate: false and retain normal age-based cleanup. Other worktrees require a lease runId matching a terminal persisted job before early merge detection; the lease status itself is only a checkout-time snapshot.

Idempotent email draft creation

POST /api/drafts accepts optional idempotency_key (nonempty string, at most 240 characters). Keys are scoped to the authenticated user. The first request atomically stores the draft and a checkpoint containing quell_email_id and the draft ID, returning 201. Repeating a key returns the existing draft with 200, even after it was sent; new request content does not overwrite it. Receipt lookup runs before sender/signature preparation, so mailbox disconnection or reconfiguration does not prevent replay. Database failures return HTTP 500 with a JSON { error } body. If the draft has been deleted (including soft deletion with status discarded), the endpoint returns 409 with draft_id, replayed: true, and a localized error. Clients must use a new key only for an intentionally new operation. Requests without a key retain normal creation.

Agent Orchestrator Claude Cloud (experimental)

repoConfigs adds claudeCloudEnabled (default false) and claudeCloudBranch. POST /api/modules/agent-orchestrator/api/config accepts cloudRepository: { provider: "claude", repo, integrationName, claudeCloudEnabled, claudeCloudBranch } to patch only this provider's fields. The repository must already exist in configuration; enabling requires GitHub and a valid branch. No migration is required. Codex cloud fields and per-automation models are preserved.

GET /api/modules/agent-orchestrator/api/claude-cloud-status returns { available: true } or { available: false, code }. Codes include claude_cloud_auth_required, claude_cloud_auth_expired, claude_cloud_auth_scope, claude_cloud_access_denied, and claude_cloud_transport. Readiness checks may renew an expiring account token in its private CLI cache; they never submit tasks or return credentials or account diagnostics. The check verifies account access only, not repository execution.

POST .../jobs accepts executionTarget: "cloud", provider: "claude", model for a configured Claude repository. The existing repository/integration fields select the exact configuration. Local attachments and Codex goal mode are unavailable. This manual target remains binding on failure. Automatic jobs can use the existing compatible-target fallback. Cloud checkpoints add provider: "claude", requestId, sourceTree, submissionPrompt, requestedModel, reportedModel, and optional verificationError; only service-reported model identity is used for completion attribution. The original task stays unchanged for Restart; the generated snapshot contract is stored in submissionPrompt. Manual output is read-only and retained for inspection; follow-up and cancellation happen at the returned Claude URL.

The adapter uses the official interactive CLI for submission, a separate Linux CLI credential directory, and bounded read-only Anthropic session/event requests for tracking. It requires explicit successful completion plus a complete report/patch envelope with matching request ID, host baseline, verified initial source-tree hash and actual model. The Cloud snapshot has its own seed commit; host commit identity is rechecked before applying its patch. Unknown schemas, model mismatches and incomplete patches fail closed. See the module documentation for account setup and the live-validation boundary.

Tracked-PR blocker diagnostics

Agent Orchestrator status recent follow-up failures retain error, failedAt, attempts, requiredValidationCapabilities, and interruptedJobId; external blocked results additionally expose blocked: true, retryAt (Unix milliseconds), headSha, baseSha, snoozeKey, and blockerSignature. These fields describe durable retry admission, not a passing CI check. Follow-up agent result JSON accepts optional retry_after as an absolute ISO 8601 timestamp with timezone (Z, ±HH:MM, or ±HHMM); missing/invalid/past values or resets more than seven days after the failure use the AGENT_ORCHESTRATOR_TRACKED_PR_BLOCKED_BACKOFF_MS initial blocker backoff (default one hour), doubling for repeated identical blocker/head findings up to seven days; host faults use AGENT_ORCHESTRATOR_TRACKED_PR_HOST_BLOCKER_BACKOFF_MS (default ten minutes) instead. Ordinary execution failures retain their existing retry policy.

Video Studio explicit scene mute

  • POST /api/video-studio/ai/scenes/:id/audio with { "mode": "muted" } requires an authenticated workspace writer. Returns {scene} after the atomic state change. Only a clip_ready scene in an idle project can change. Other modes are rejected. Idempotent for a muted scene.
  • GET /api/video-studio/ai/scenes/:id/audio requires workspace read access and serves the active silent derivative with range support and private, no-store caching. It is not a public media URL.
  • video_studio_mute_scene({scene_id}) is the equivalent agent operation.
  • Existing STS DELETE/reset retains its narrower meaning: discard voice conversion, keep TTS and embedded audio. Full mute is explicit.
  • Internal bearer tokens for /api/video-studio/voiceover use modules:api:read/write; membership and role checks still run inside the routes. Voiceover GET returns 404 with code=voiceover_not_found for absent project manifests.

Mute writes a new video-only file and publishes metadata.audioOverride with mode=muted and clipPath; clears script, voiceoverText, dubApplied, dubPreview, dubPath, dubStatus and voiceChange; invalidates worker tokens; retains all original/previous media. State publication locks the project and compares the scene snapshot, rejecting concurrent edits. Background claims, completion and durable-file reconciliation respect the mute tombstone.

Scene mute lifecycle

Mute applies to the current scene clip. Regenerating/replacing an AI clip or HTML source clears the override; editing script or voiceover text also clears it so narration can be generated again. There is no standalone unmute mode; STS reset only discards voice conversion. Existing final versions remain unchanged. A missing silent derivative returns 404. Bundle-only suppression applies only during an active generation/concatenation run and does not suppress later narration edits. Web, iOS, macOS and agent operations share these server rules; no client controls change.

Personal email workflow recovery

Apply 314_email_automation_checkpoints.sql and 312_email_draft_checkpoints.sql before deploying. The original draft checkpoint filename is preserved so previously migrated instances do not rerun it. The runner tracks full filenames; the shared 312 prefix with the memory migration does not prevent either file from applying. Also apply 315_email_prepared_document_checkpoints.sql before deploying the updated web workflow. Prepared document inserts/updates and their durable result receipt commit in one transaction; retries reuse the document without rewriting human edits, and deletion leaves a tombstone rather than recreating it. When UIDVALIDITY changes, reopening the personal delivery queue and removing the old generation's automation state commit atomically under the web workflow's per-mail lock. Existing drafts, documents and tasks are retained (obsolete draft-to-UID links are detached to prevent sending an old draft from settling the new mail), but new-generation draft/document checkpoints, model sessions and task deduplication use a distinct identity. Raw IMAP UIDs in public email links and automation rows stay unchanged. The internal personal-automation endpoint validates uidValidity before processing and acknowledgement; stale generations return HTTP 409 with settled: false. Attachment callbacks that omit uidValidity pin the queued generation before processing; explicit null still identifies the legacy generation. Multi-message calls must provide uidValidity (otherwise HTTP 400). Non-2xx resume responses are logged.

Personal workflow receipts use personal_email_draft_checkpoints, separate from the user-supplied operation keys used by /api/drafts. Personal workflow retries preserve edits and treat deleted or discarded checkpointed drafts as settled decisions. The queue covers newly ingested messages; historical failures are not backfilled. Delivery runs after mailbox ingestion, prioritizing fewer-attempt rows, with a lease that grows from ten to sixty minutes and no fixed retry limit. Claims remain monotonic for stale-acknowledgement fencing. HTTP success with pending work clears last_error; delivery failures retain it and emit a warning. Previously exhausted rows are eligible again, so outages cannot permanently abandon the backlog. POST /api/internal/emails/automation/personal returns settled: true for disabled analysis, deliberately ignored messages, and persisted terminal workflow states. Missing cached details are fetched from the mailbox. Confirmed absent IMAP messages and persisted failed workflows settle; failures remain visible in the workflow for operator review. Unresolved details and running workflows remain pending. The native and Live emails_create_draft tools accept an optional idempotency_key for deliberate replay. Source IDs alone do not deduplicate interactive drafts.

Attachment generation fencing

Personal email attachment receipts retain legacy source IDs for the initial UID generation. Reopened UIDs use encoded email-attachment-v2 source IDs containing the automation generation identity as well as the raw UID. Cached document links are revalidated against these receipts; a failed import remains pending rather than falling back to content from an older generation.

The document indexer updates automation context only while holding the shared mailbox/UID lock and after matching both the current generation and the document ID in source_attachments. Resume requests to /api/internal/emails/automation/personal carry automationSourceId and uidValidity; stale callbacks return HTTP 409 without running the workflow. Successfully persisted ignored messages are terminal settlements; failed ignore persistence remains retryable. No new schema migration is required.

Personal IMAP task source links pass owner as the detail API's mailbox and generation as its generation parameter. A generation-bearing GET /api/emails/:id requires the exact configured mailbox and INBOX, validates the queued identity, bypasses unversioned detail caches, and checks the actual IMAP UIDVALIDITY before reading or marking the message. A stale or ambiguous link returns HTTP 409 (stale_email_generation), not another message with the same UID. The web opener retains this fence when retrying the link and clears it when explicitly navigating to another inbox message. Generation-bearing detail reads also pin attachment receipt identity and validate UIDVALIDITY on follow-up IMAP connections. Attachment import and cache publication run under the same generation lock as ingestion; rollover before publication rejects the request rather than launching an unfenced background import.

Email recovery conflicts and replay

Personal email attachment and inline endpoints accept the same optional generation identity as historical task source links. A stale generation is rejected with HTTP 409 before IMAP bytes are fetched. Email automation actions return localized HTTP 409 on the one-second generation-lock timeout; clients may retry once the active run finishes. Automation discard reasons honor ui_language (DE/EN/IT), and replay of a sent draft reports sent, not reply_prepared. These server contracts apply to web, native and agent callers; no new tool is needed. See runtime recovery for migration 317, independently retryable indexer callbacks and bounded queue delivery.

Historical personal-email task links also carry their opened generation and exact mailbox through automation POST (start), GET (poll), and action POST requests. Generation-bearing requests validate the queue identity and live IMAP UIDVALIDITY before resolving cached raw-UID content; start/task/calendar mutations revalidate UIDVALIDITY under the workflow lock. Stale requests return localized HTTP 409 with reason: stale_email_generation without marking the replacement workflow failed or creating tasks/calendar entries. Polling retains the fence on every retry and stops on stale-generation conflicts. Requests without generation retain the existing inbox/provider behavior; migration 317 is unchanged.

Runner 0.3.5+ reports its IANA timeZone. Fleet quota failures, including manual probes, quarantine that runner/provider until the reported reset (interpreted in the worker timezone), or four hours when the reset cannot be determined. They never retry another model on the same exhausted account. Other providers and machines remain eligible. Remote modelAttempts include runner, errorClass, and retryAfter (epoch milliseconds); Fleet cooldowns exposes the persisted reason and deadline.

Chat foreground timeout

For native POST /api/chat streaming turns, an absolute 60-second foreground budget ends the SSE response with data: {"type":"clapilot.handoff", "runId":"...","message":"<localized status>"} and data: [DONE]. The request entry timestamp supplies the deadline; progress and fallbacks do not reset it. The original assistant message remains pending until the same background producer finishes. Clients should refresh/poll chat history following stream termination to retrieve pending state and the eventual answer; stream termination alone is not proof that the agent task completed. Web, iOS and macOS clients recognize the handoff event, retain pending/tool state, skip completion notifications, and refresh history. The handoff message is status, not answer content. A producer/startup failure errors the connected stream after interrupted-answer persistence; it never produces a successful empty EOF. Tool payloads, run ids and authorization do not change. Request preparation before stream construction is not forcibly cancelled. Long work retains existing process lifetime/restart-reconciliation guarantees, not a new durable queue guarantee.

Fleet validation capabilities

Agent Orchestrator Fleet registration/heartbeat payloads and serialized runner status include optional validationCapabilities: ["docker"]. The runner advertises Docker only after docker info and docker compose version succeed; probes are cached for at most 60 seconds. Missing capabilities mean unverified support. Persisted follow-up jobs carry requiredValidationCapabilities, used together with normal runner freshness, provider compatibility and slot limits.

Hub outage lifecycle: DELETE /api/hub/health/instances/[id] records retirement on existing outage-task links before removing the instance. Host PATCH updates preserve those links by instance ID. Task creation/update with a new unknown-host monitoring-outage:<hostname> tag returns the existing 409 HUB_OUTAGE_RECOVERY_REQUIRED response immediately. Apply migration 318 after 314.

Remote orchestrator runner heartbeat and claim requests include optional storage telemetry: host name, checkedAt epoch milliseconds, minFreeBytes, healthy, and volumes containing path/free-byte pairs. Missing telemetry remains protocol-compatible but cannot admit new jobs. Runner inventory returns this telemetry and storageHealthy. Freshness is 60 seconds; the default free-space floor is 20 GiB. See the Agent Orchestrator storage/retention section for configuration and recovery.

Native run safety failures

Native run failures may report error_code: content_policy_blocked. For xAI SAFETY_CHECK_TYPE_BIO / permission-denied, diagnostics expose usage_json.runDiagnostics.error.policyCategory: bio and providerCode: permission-denied. The same identifiers appear on provider attempts with decision: refuse_policy. Missing policy identifiers are omitted or null; clients must not offer credential reconnect for this error code. The existing inference gateway maps the failure to HTTP 400 with content_policy_violation. No database migration is required.

Image Playground generation jobs

Image Playground text-to-image requests (including reference images) now use short HTTP requests and a durable PostgreSQL job. Web, iOS, and macOS wait for status updates instead of keeping a provider request open through a proxy. Slow providers can finish after the proxy's request timeout. Reopening the Playground recovers recent workspace jobs and the latest older output explicitly shared to the Image Playground media-library folder; private chat images and reference/mask uploads are excluded. Existing gallery entries are deduplicated by asset ID. The personal canvas/history layout remains unchanged.

  • POST /api/generated-images/generate and POST /api/generated-images/edit accept their existing fields plus async: true (or ?async=true), library_source: "image-playground", and a stable request_id (16–128 letters, digits, underscores or hyphens; UUID recommended). They return HTTP 202 with { job: { id, status, kind, prompt, result } } before calling the provider; kind is generate or edit. Retries with the same creator/request ID return the original job and ignore replacement payloads. Edit jobs keep image_id, reference_image_ids and mask_image_id in the stored request and run through the same edit executor as the synchronous endpoint.
  • GET /api/generated-images/jobs?id=<uuid> returns { jobs: [...] } with queued, running, ready, or failed. Only ready has a completed result.asset and result.markdown; failures have result.error. Responses are authenticated and uncached. Omitting id recovers up to 80 recent Image Playground jobs, plus the latest legacy shared asset when capacity allows. Jobs/output are workspace-global; creator IDs are used for attribution and idempotency, not read filtering.
  • The web client polls every three seconds and retains the request ID across ambiguous POST failures. Poll/network/proxy failures keep it waiting instead of re-submitting a generation. Apple clients offer reload/recovery after connection errors and lock new generation until that recovery succeeds. All status/error UI is localized in German, English and Italian.
  • images_generate supports async: true with request_id for Image Playground output, and job_id for subsequent status checks (the required prompt is ignored for status checks). Agent responses must describe queued/running jobs as still generating and only present a result once ready. Synchronous generation for private chat and Notes insertion keeps its existing contract, and images_edit stays synchronous for agents.

Deployment requires migration 323_generated_image_jobs.sql before updated clients/API. Generation is executed after the HTTP response in the persistent Node server, protected by a PostgreSQL session advisory lock. Polling also resumes queued jobs. A process restart reconciles a running job against generated_images.metadata.generation_job_id; a saved asset is recovered, otherwise the job fails with generation_interrupted instead of silently repeating a potentially chargeable provider call. A server restart cannot resume an upstream provider call whose result had not yet reached local storage.

Async Image Playground recovery compatibility

Generation accepts async: true in the body or ?async=true for existing clients. Clients may send display_prompt separately from the provider prompt; recovery uses that display text and removes the legacy reference-only instruction when absent. Job reads remain workspace-global, but only the creator receives private reference IDs and source paths. Other members receive the output asset without source IDs or metadata. Web, iOS, and macOS restore completed jobs on opening the playground and skip active workspace jobs during initial load so another user's provider cannot lock the gallery. Reopen after completion to recover those jobs; newly started jobs continue polling and retry transient connection failures with the same ID.

Image Playground gallery stacks

Playground state versions accept an optional stackId (string, up to 64 characters), preserved by GET/PUT /api/modules/image-playground/api/state. Clients assign one ID per original generation/upload and inherit it for edits, including branches from older versions. Legacy consecutive edits are grouped by their saved order. Gallery deletion and multi-selection remove all versions of selected stacks; confirmation text explicitly includes those versions. The 80-version retention limit is unchanged. GET /api/generated-images/jobs exposes optional image_id for edit jobs so recovered results join the source image's stack, rather than the currently open canvas. Unknown sources remain separate. The existing images_generate / images_edit agent contracts are unchanged: agents generate/edit assets but intentionally do not manage personal Playground stacks or editor history.

Chat skill autocomplete

GET /api/chat/skills?search=<query> requires an authenticated session or the existing modules:api:read service scope. It returns { skills: [{ id, name, description }] }, up to seven alphabetically sorted active, enabled installed skills matching the name, directory name or description. The response uses Cache-Control: no-store and excludes filesystem paths and skill bodies; unauthenticated requests return 401. Installed-skill discovery uses the runtime catalog and its existing 60-second cache.

Selections are serialized as /skill:<id> tokens in the existing chat message string. Repeat tokens to select multiple skills; the runtime deduplicates them and loads all selected files before the agent runs. See Skills for lifecycle and error behavior.

Social Media short text for X

POST /api/modules/social-media/api/posts and PATCH /api/modules/social-media/api/posts/:id accept optional xContent (string). Post responses expose xContent, stored in the existing metadata.xContent field; no schema migration is needed. Metadata-based clients remain supported. An omitted value preserves an existing short version, and an empty value derives a shortened fallback from content. Explicit oversized text returns HTTP 400 with code: x_text_too_long and a localized message. The conservative budget is 280 weighted characters (NFC, Unicode weights, at least 23 per URL-like token). Main content and other target texts are unchanged.

POST /api/modules/social-media/api/generate additionally returns xContent alongside title, content, and hashtags. Clients must not append hashtags to the X version. Immediate and scheduled publishing send metadata.xContent only to X; legacy drafts without that field use the same deterministic shortening fallback displayed in the editor.

Task board deletion

DELETE /api/aufgaben/boards/:id accepts an optional JSON body { move_tasks_to_board_id?: UUID, ui_language?: "de" | "en" | "it" }. Existing UI callers may continue using target_board_id. Empty boards need no target. Non-empty boards require an accessible destination; tasks are moved atomically before deletion. force is unsupported and returns 400. Same-board targets and protected Standard boards return 400; active self-acting agent boards return 409. Missing/inaccessible sources return 404; missing/inaccessible targets return 400. Session authentication or the existing modules:api:write token is required. Shared/private ACLs are identical to UI board operations.

Success: { id, name, moved_task_count }. An authorized retry returns { id, name, moved_task_count: 0, already_deleted: true } with HTTP 200 and no duplicate audit. The durable audit in aufgaben_board_deletion_audit is part of the same transaction. Agent callers use aufgaben_delete_board via the tool API so configured delete approvals remain enforced.

Team Chat voice rooms

POST /api/chat/group/rooms accepts optional communicationMode: "text" | "voice" (default text). Voice mode requires a public/private channel. Room summaries expose communication_mode; existing message APIs are unchanged.

POST /api/chat/group/rooms/:id/voice authenticates the human, requires a current accessible voice room (private rooms require active membership), and returns { url, token, revision } with Cache-Control: no-store. Credentials last 60 seconds and authorize microphone and camera publishing (screen sharing and data publishing remain disabled). GET on the same path returns { available: true, revision } for permission rechecks. Responses: 401 unauthenticated, 403 inaccessible/archived/non-voice, 503 voice service unconfigured. Tokens never appear in agent results.

team_chat_voice_rooms accepts action: "list" | "create", name (required for create), optional visibility: "public" | "private" (default public), and optional member_user_ids. It runs under the caller's identity and returns accessible rooms or the created room plus /team-chat?room=.... Chat/live agents use this feature-specific tool to create/discover rooms, then direct the human to join and enable their own microphone or camera. Main-agent-only mutations follow the existing Team Chat policy. Text tools continue to work. Optional room assistance uses the additional status and configure actions. Both accept room_id (defaults to the current room). configure requires a complete settings object: voice_agent, text_agent, transcription booleans and translation: null | "de" | "en" | "it". Optional realtime_provider selects an enabled provider slug; null follows the instance default. Only room managers with listening access may configure it, and only on explicit user request. Agents and translation require transcription. Microphone activation and camera publishing remain human-only or unsupported. See voice rooms.

GPT-6.1 Sol model selection

Existing provider config, chat model refs, model_routing.priority, and Agent Orchestrator coding-model contracts accept gpt-6.1-sol for OpenAI API and Codex subscriptions. Migration 344 appends the ID without changing defaults. API preflights and text/tool execution use /responses. Codex discovery uses the installed app-server catalog (Docker pins 0.159.0). No API shapes change; see Providers and models for reasoning, limits, and pricing.

Optional voice-room assistance API

GET /api/chat/group/rooms/:id/voice/assistance returns {settings, revision, status, canManage, providers, segments}. PUT accepts {settings, revision} and uses optimistic concurrency (409 on a stale revision). A first configuration uses revision: null. For native Codable compatibility, omitted nullable settings (translation, realtime_provider, realtime_model) are normalized to null; omission disables translation rather than preserving its previous value. The three listening/agent booleans remain required, and agents still require explicit transcription consent. Authentication and voice-room access are required; private rooms require actual membership even for workspace admins. Updating requires room management permission. Errors are localized. Settings default off.

The internal POST /api/agent-runtime/team-voice bridge requires the existing agent internal secret. poll acquires/renews a twenty-second room lease; audio, translation, response, voice_response, validate and status require its owner, settings revision and room revision. Audio segments have unique UUIDs for deduplication. Provider credentials never reach browser/native clients or agent tools. ClapilotAICore owns listening, response decisions, tools and speech output; the web app owns room permissions, configured audio providers and durable captions. No captured audio or automatic response is forwarded to external channel mappings.

Realtime model catalog

Voice-room provider function/delegation requests now use the existing speaker-scoped ClapilotAICore execution path. The bridge's validate action checks the verified speech segment, lease and current speaker access before execution and publication; no new public API fields or client credentials are required. The optional text-agent setting controls written backend results when a voice agent is active. Human barge-in clears playback without sending a new conversational silence instruction. See Team Chat voice rooms for attribution and approval behavior.

Authenticated GET /api/realtime/models returns models: [{providerSlug, providerLabel, providerType, model, isDefault}] and canEdit. Administrators also receive compatible provider choices and model suggestions in providers. Admin-only POST /api/realtime/models accepts {models: [{providerSlug,model,isDefault}]}. It validates provider/model compatibility, deduplicates pairs and keeps exactly one preferred model in a nonempty list. The catalog is stored as app_settings.media_model_catalog.realtime; its preferred pair updates the existing realtime routing default. Empty lists remain empty.

Voice assistance settings additionally accepts realtime_model: string|null, paired with realtime_provider; both null follow the preferred catalog model. GET includes curated models and a safe error_code (quota, authentication, model_unavailable, provider_error, or null). Worker jobs resolve the exact current pair and reconnect when the preferred pair changes. Private-room and manager checks remain in force.

Voice transcript history and chat messages

GET /api/chat/group/rooms/:id/voice/transcript?before=<segment-id> returns {segments, nextCursor}. Each segment contains id, speaker_name, transcript, translated_text, language, and created_at. Pages contain up to sixty sentences in chronological order; omit before for the latest page, then pass nextCursor to load older entries. Equal timestamps use the segment ID as a stable tiebreaker. A null cursor ends pagination. Authentication, non-archived voice-room access, and actual membership in private rooms are required; workspace administration does not bypass membership. Responses are private and uncached. Invalid cursors return 400.

team_chat_voice_rooms supports action: "transcript", room_id (defaults to the current room), and optional before; it returns the same page under the caller's identity. This is read-only and does not enable listening. Voice settings live in a popover; the transcript button opens a separate reader.

Migration 348_voice_transcript_chat_messages.sql links each new utterance to one speaker-attributed chat_group_messages row via team_voice_segments.transcript_message_id. Translation updates this row in place. Its metadata has source: "team_voice_transcript" and team_voice_segment_id. Text-agent replies retain a separate message. Publishing these rows bypasses external-channel forwarding and ordinary chat-agent dispatch; only explicitly enabled voice-room assistance processes speech. Existing historical transcripts are not backfilled into chat.

Team Chat participant cameras

Existing voice rooms also support opt-in participant video on web, iOS and macOS. Users join with camera and microphone off. The camera button requests device permission, publishes only the camera track, and shows a local preview alongside remote cameras in a bounded horizontal strip. Turning it off stops camera capture; leaving, switching rooms, or losing room access disconnects capture and playback. No new API fields or migrations are required. team_chat_voice_rooms still creates and discovers these rooms through the existing room links. Agents must direct the human to the camera control: remote camera activation is intentionally unsupported. Room AI assistance consumes audio only; video is neither recorded nor forwarded to model providers.

Module storage also preserves canonical code targets linked into data/, including deletion of their ancestors, and declared module entry files. Unresolvable module symlink loops remain inaccessible without disabling unrelated ordinary File Explorer writes.

The code boundary also rejects mutation aliases sharing an executable file inode through a hard link. Hard links between ordinary module data files remain usable. Dangling module link chains protect their eventual targets without disabling unrelated workspace writes.

Canonical module-code protection uses a fresh filesystem inventory across workspace, managed, and bundled collections with the same manifest identity validation as module discovery. Code links and shared inodes from any validated module protect their targets from storage mutations in every other module. Ordinary data/ trees are excluded from the code scan; declared entries/workers and links from code into data remain protected. Invalid collection entries and inaccessible subtrees do not disable unrelated File Explorer mutations. Collection directories and inaccessible code subtrees retain their read-only boundary.

A valid manifest establishes module-code protection before its entry exists, preventing activation through dangling links. Manifest paths and shared inodes retain activation protection even while invalid. Storage also treats every canonical module root as code outside that module's own data/, including modules installed inside another module's data directory; both modules' ordinary data remains writable. The shared protection helper is packaged inside File Explorer so relocating a complete module preserves its runtime imports.

Dangling module-directory aliases reserve their eventual manifest path before the directory exists, so a multi-file upload or prepared-directory rename cannot activate code through an otherwise writable canonical target.

Code-link protection includes every intermediate symlink path and reachable cycle member as well as its final target, preventing deletion or renaming of link ancestors. Write-protection inventories do not add module collections to ordinary readable roots. Malformed manifest coercions and non-file manifests are isolated consistently with module discovery and do not disable unrelated mutations; their manifest activation boundaries remain protected.

Ultrafast model speed

Provider discovery serviceTiers / service_tiers includes ultrafast when Codex advertises it. Provider metadata.modelServiceTiers[model] and Agent Orchestrator codexServiceTier accept ultrafast alongside fast / flex; existing shapes and defaults remain unchanged. Codex thread/turn and coding CLI requests preserve the selected tier; first-party OpenAI API Responses uses service_tier. This option changes execution speed, not reasoning effort. Current availability is Astra; Sol follows runtime discovery. See Providers and models.