Skip to main content
The agent API is Vexa’s control plane. It turns triggers into dispatches and streams their output; it does no execution itself — it validates and hands off to the runtime. Endpoints are fronted by the gateway, which carries authentication and per-route scopes; the service itself is internal.
Identity is server-derived. Public clients reach these routes through the gateway under the canonical prefix /agent/*. The gateway resolves your X-API-Key → user and injects X-User-Id; agent-api derives the workspace/chat/quota subject from that header (P20). A subject in a request body or query string is ignored — never trust the client for identity. The $AGENT_API/api/... paths below address the internal service directly (which requires X-User-Id); through the gateway the same routes are $API_BASE/agent/... with X-API-Key.

Health

GET /health

Models

GET /api/models → the resolved model names for this subject:

Dispatch a unit

POST /invocations — the sink every trigger funnels through. Body is a conformant unit.v1 Invocation (the same envelope units.make_dispatch builds); it is validated and dispatched to the runtime. 400 if the envelope is non-conformant.
POST /invocations → 202
The shape is: identity.subject + identity.launcher, runner, a workspaces list (mode is rw for trusted triggers — message/scheduled — else ro), the trigger, and a start that is either {"entrypoint":{"inline":"..."}}, {"entrypoint":{"path":"..."}}, or {"session":{"ref":"..."}}. There is no top-level subject, workspace_repo, or plan field.

Chat (streamed)

POST /api/chat — body { prompt, session?, active? } (subject is server-derived, not sent). Returns Server-Sent Events; each data: line is one frame:
POST /api/chat (SSE)
POST /api/chat/reset { session? }{ "ok": true } · GET /api/sessions{ "sessions": [...] } · GET /api/sessions/{session}/history{ "turns": [...] } (tolerant — a missing transcript returns { "turns": [] }).

Routines

POST /api/routines → 201
GET /api/routines{ "routines": [...] } · PATCH /api/routines/{name}/enabled { enabled }{ "ok": true, "name", "enabled", "reconcile" } · DELETE /api/routines/{routine_id}{ "ok": true, "routine_id": "..." }.

Events

POST /events — body is an event.v1 Event (an integration firing). It maps to a unit.v1 Invocation and dispatches. 400 if non-conformant; 422 if the event carries no plan.
POST /events → 202

Live meeting copilot

A live-meeting copilot is itself driven through make_dispatch; the id invariant is meeting_id == session_uid == native_id.
  • POST /api/meeting/start { native_id, platform, title? }202, returns the live-meeting record.
  • POST /api/meeting/process { native_id, platform, on } → toggle opt-in processing. on:true resumes from the per-meeting cursor ({ ..., "processing":true, "resumed_from":"<id>" }); on:false clears the flag and freezes the cursor ({ ..., "processing":false }).
  • GET /api/meeting/relay-health → typed transcript-relay health (a stale bot key surfaces as native_resolve:{ok:false,kind:"unauthorized"} rather than silent dead air).
  • GET /api/meeting/stream?meeting_id=&session_uid= (SSE) → the merged transcript + copilot-output feed. Resumable: every event carries an SSE id:; on reconnect echo it as Last-Event-ID to resume gaplessly. Event types: transcript, card, message-delta, tool-call, ping, meeting-end.

Workspace

GET /api/workspace/tree?hidden={ "files": ["kg/entities/..."] } · GET /api/workspace/file?path={ "path": "...", "content": "..." } (404 if absent) · GET /api/workspace/git{ branch, changes, commits } · POST /api/workspace/upload (multipart files, ≤25 MB each) → { "files": [{ name, path }] } · POST /api/workspace/init201 { workspace, seeded, already_initialized } (idempotent) · GET /api/workspace/attached{ active, parked } · POST /api/workspace/swap { repo?, ref?, token? } → attach an external git repo as the active workspace (parks the current one; omit repo to swap back to the seed). (All workspace routes derive subject from X-User-Id — no subject query parameter.)