API Reference
The Go backend exposes a REST API under /api/v1 and an event channel at /ws. This reference is for anyone integrating or automating: it covers authentication, error conventions and the main routes as registered in the router, with examples you can copy.
On this page
- Base URL and versioning
- Authentication
- API tokens for integrations
- Errors, limits and conventions
- Requests, chat, tasks and reports
- Approvals and agents
- Projects and skills
- Shop, rewards and drive
- Connections, controls and settings
- Generic API sources
- WebSocket and health
Base URL and versioning#
All REST routes hang under /api/v1; the version is part of the path. The WebSocket (/ws) and the root health check (/healthz) live outside that prefix. Bodies are UTF-8 JSON, except drive uploads, which send the file as the raw body.
Examples use localhost:8080; substitute your host. Every route on this page is relative to /api/v1 unless stated otherwise.
# REST base URL (local default)
http://localhost:8080/api/v1
# WebSocket (outside /api/v1)
ws://localhost:8080/ws
# Health check: no token needed
curl http://localhost:8080/healthz
# {"status":"ok","runtime":"up","runtime_mode":"simulation"}
Warning
With AUTH_ENABLED=false (demo mode) the API requires no authentication and everything runs on one fixed organization. Do not expose that mode to a network.
Authentication#
With AUTH_ENABLED=true, every route requires a token except /healthz, the public /auth/* routes, the OAuth callback and a few links that carry their own secret (signed approval links and the calendar feed).
Login returns an access token (a JWT, 15 minutes) sent in the Authorization: Bearer header. The refresh token never appears in the JSON body: it travels in the HttpOnly, SameSite=Strict cookie wfai_refresh, scoped to the /api/v1/auth path. Refresh and logout act on that cookie, so they also require the X-WFAI-CSRF header.
The organization comes from the token, never from a parameter. Roles are owner, admin, member and viewer, and each route requires a specific permission.
# Sign in: access token in the body, refresh token in an HttpOnly cookie
curl -i -X POST http://localhost:8080/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"<password>"}'
# 200 {"token_type":"Bearer","access_token":"<token>","expires_in":900,
# "expires_at":"...","user":{...},"org_id":"<org id>","role":"owner"}
# Call the API with the access token
curl http://localhost:8080/api/v1/agents -H 'Authorization: Bearer <token>'
# Renew the session: cookie + CSRF header
curl -X POST http://localhost:8080/api/v1/auth/refresh \
-b 'wfai_refresh=<cookie value>' -H 'X-WFAI-CSRF: 1'
| Method and path | Description |
|---|---|
POST /auth/register | Creates an organization and an owner user. Body: email, password, name, org_name. Returns 201 with the session, or 202 with verification_required when email verification is on. |
POST /auth/login | Signs in. Body: email, password, optional org_id. |
POST /auth/mfa/verify | Completes a login that answered mfa_required. Body: mfa_token, code. |
POST /auth/refresh | Rotates the refresh token (cookie + X-WFAI-CSRF) and returns a new access token. |
POST /auth/logout | Ends the current session and clears the cookie. Returns 204. |
POST /auth/logout-all | Revokes all of the user's sessions. Returns 204. |
POST /auth/switch-org | Opens a session in another of the user's organizations. Body: org_id. |
GET /auth/me, GET /auth/config | User, role, permissions and organizations; public configuration (whether login, registration, verification, trial and MFA are on). |
GET/POST /auth/members, PATCH/DELETE /auth/members/{id} | List, add (email, role), change the role of, or remove members. |
GET/POST /invitations, DELETE /invitations/{id} | Invitation management. POST /auth/invitations/accept accepts them. |
Note
If the user has two-step verification on, POST /auth/login answers 200 with mfa_required and an mfa_token instead of the session. Accounts, roles and sessions are covered in depth in Authentication and multi-tenancy.
API tokens for integrations#
For server-to-server integrations there is a separate API at /api/public/v1, authenticated with scoped API tokens instead of user sessions. The organization comes from the token. It needs Postgres; without it these routes are not mounted.
An admin creates, lists and revokes tokens with POST/GET /governance/tokens and DELETE /governance/tokens/{id}. Creation body: name, scopes, expires_in_days. Each token allows 120 requests per minute; beyond that it answers 429 with Retry-After.
GET /agents: scopeagents:read.GET /requests,GET /requests/{id}: scoperequests:read(also covered byrequests:write).POST /requests: scoperequests:write. Same body as the session API (text,budget_cap_usd); answers 202 withrequest_id.GET /reports,GET /reports/{id}: scopereports:read.GET /usage: scopeusage:read.
# Create a token (admin session); the plaintext is returned once
curl -X POST http://localhost:8080/api/v1/governance/tokens \
-H 'Authorization: Bearer <session token>' -H 'Content-Type: application/json' \
-d '{"name":"crm-sync","scopes":["requests:write","reports:read"],"expires_in_days":90}'
# 201 {"token":"<api token>","item":{...},"note":"shown once"}
# Use it against the public API
curl -X POST http://localhost:8080/api/public/v1/requests \
-H 'Authorization: Bearer <api token>' -H 'Content-Type: application/json' \
-d '{"text":"Summarize the open deals of this week"}'
# 202 {"request_id":"<id>"}
Warning
The plaintext token is returned once, at creation. Store it in your secret manager; if it leaks, revoke it with DELETE /governance/tokens/{id}.
Errors, limits and conventions#
Errors return JSON with a meaningful HTTP status. There are three shapes depending on the route; a 500 never exposes internal detail.
- No global pagination: each listing defines its own parameters.
GET /activityacceptslimit(default 100, maximum 1000).GET /conversations/{id}/messagesacceptslimit(maximum 200) andbefore, and returns theX-Has-MoreandX-Next-Beforeheaders.GET /conversationsacceptslimitandoffsetand returnsX-Has-MoreandX-Next-Offset.GET /rewards/ledgeracceptslimit(default 50, maximum 200) andbefore;GET /agents/{id}/memoryacceptslimitandq.- JSON bodies are capped at 1 MiB by default (
MAX_BODY_BYTES); beyond that the response is 413. - The public authentication routes have per-IP rate limits and answer 429 with
Retry-After.
// Most routes
{ "error": "invalid input: text is required" }
// Some domains (connections, controls, drive, shop...) add a stable code
{ "error": "conflict: not enough coins", "code": "insufficient_coins" }
// /auth/* routes and permission failures
{ "error": { "code": "forbidden", "message": "insufficient permissions" } }
// Auth validation (422) lists the fields that failed
{ "error": { "code": "validation_failed", "message": "invalid input",
"fields": { "password": "..." } } }
| Code | Meaning |
|---|---|
| 400 | Invalid body or parameters. |
| 401 | Missing, invalid or expired token. |
| 403 | The role lacks the permission, or the CSRF check failed. |
| 404 | The resource does not exist in your organization. |
| 409 | State conflict (for example, a different expected version or insufficient coins). |
| 413 | Body too large. |
| 422 | Validation failed on the /auth/* routes; fields says which field failed. |
| 429 | Rate limit, or account locked after repeated failed logins. |
| 503 | Feature not configured on the server, for example kek_missing when saving credentials without CONNECTIONS_KEK. |
Tip
POST /messages and POST /shop/purchases accept the Idempotency-Key header: a retry with the same key returns the first result instead of repeating the action.
Requests, chat, tasks and reports#
POST /requests is asynchronous: it answers 202 with the request_id and progress arrives over the WebSocket. budget_cap_usd is optional and sets a hard cap for that request. GET /requests/{id} returns the request with its tasks, plan and accumulated cost.
Request states: planning, running, awaiting_approval, done, failed. Task states: pending, running, blocked, awaiting_approval, done, failed.
# Submit a request: asynchronous, progress arrives over the WebSocket
curl -X POST http://localhost:8080/api/v1/requests \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"text":"Prepare a $50,000 proposal for ACME","budget_cap_usd":5}'
# 202 {"request_id":"<id>"}
# Chat with the office (a retry with the same key returns the first turn)
curl -X POST http://localhost:8080/api/v1/messages \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-H 'Idempotency-Key: 7d1e...' \
-d '{"conversation":"office","text":"Who is free today?"}'
# 202 turn object; replies arrive as chat.message events
{
"id": "<request id>",
"text": "Prepare a $50,000 proposal for ACME",
"status": "running",
"created_at": "2026-01-01T12:00:00Z",
"report_id": null,
"cost_usd": 0.0123,
"tasks": [
{
"id": "<task id>",
"request_id": "<request id>",
"workflow_id": null,
"title": "Draft the commercial proposal",
"description": "...",
"agent_id": "sales",
"status": "running",
"depends_on": [],
"parent_task_id": null,
"created_at": "2026-01-01T12:00:02Z",
"started_at": "2026-01-01T12:00:03Z",
"finished_at": null,
"output": null
}
],
"plan": {
"objectives": ["..."],
"clarifying_questions": [],
"tasks": [
{ "id": "<task id>", "title": "Draft the commercial proposal",
"agent_id": "sales", "depends_on": [] }
]
},
"budget_cap_usd": 5
}
| Method and path | Description |
|---|---|
POST /requests | Creates a request. Body: text, optional budget_cap_usd. |
GET /requests, GET /requests/{id} | List and detail (tasks, plan, cost and cap). |
GET /requests/{id}/cost, GET /requests/{id}/estimate | Cost breakdown and upfront estimate. |
POST /requests/{id}/confirm | Confirms or cancels an estimate. Body: decision (proceed or cancel), optional budget_cap_usd. |
PUT /requests/{id}/budget | Changes the cap (budget_cap_usd; 0 removes it). Admin or owner only. |
POST /messages | Chat message. Body: conversation (office by default, or agent:<id>), text, attachments (drive paths, up to 6). Returns 202. |
GET /conversations, GET /conversations/{id}/messages | Conversations and their messages. |
POST /conversations/{id}/messages | Posts to a specific conversation. Body: text. Returns 201. |
GET /tasks, GET /tasks/{id} | Tasks; filters agent_id and status. |
GET /tasks/{id}/intervention, POST /tasks/{id}/steer, POST /tasks/{id}/reassign | Intervene in a running task. Admin or owner only. |
GET /reports, GET /reports/{id} | Final reports. |
GET /activity, GET /metrics, GET /budget | Activity, metrics and budget status. |
Approvals and agents#
Deciding an approval is a privileged action: admin or owner only. When approving you can also request a standing permission (always_allow) for internal, reversible actions; if it is not eligible, the approval stands and the response includes standing_error. An approval can require two distinct people (required_approvals).
# List pending approvals
curl 'http://localhost:8080/api/v1/approvals?status=pending' \
-H 'Authorization: Bearer <token>'
# Approve (admin or owner)
curl -X POST http://localhost:8080/api/v1/approvals/<id>/decision \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"decision":"approve","note":"ok"}'
# 200 the updated approval; "decision" is "approve" or "reject"
| Method and path | Description |
|---|---|
GET /approvals | List; status filter (pending, approved, rejected). |
POST /approvals/{id}/decision | Body: decision (approve or reject), note, scope (once, session, project), always_allow, always_allow_any_agent, always_allow_days. |
POST /approvals/batch | Decides several approvals of a project. Body: filter (project_id, action), decision, expected_count, include_high. |
GET /agents, GET /agents/{id}, GET /agents/{id}/detail | Agents: state, tools, permissions and autonomy. |
GET/PUT /agents/{id}/profile, GET /agents/{id}/traces | Versioned work profile and reasoning traces (these need audit:read). |
GET /agents/{id}/memory, DELETE /agents/{id}/memory/{docId} | Long-term memory. |
GET /role-templates, POST /agents/from-template | Hire an agent from a role template. |
PUT /agents/{id}/budget, POST /agents/{id}/control | Monthly budget (monthly_budget_usd) and pause, resume or read-only. |
Projects and skills#
A project is a multi-node plan with a budget. The cycle is create a draft, edit the plan, estimate and validate, then launch with an approved budget. Pausing, cancelling and changing the budget of a running project are admin actions.
Skills are reusable, versioned instructions. Members create and edit drafts; only an admin reviews, activates, disables or deletes them.
# Create a draft project
curl -X POST http://localhost:8080/api/v1/projects/draft \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"goal":"Launch the spring campaign","budget_usd":200}'
# 201 {"project_id":"<id>"}
# Launch it with an approved budget (required, greater than 0)
curl -X POST http://localhost:8080/api/v1/projects/<id>/launch \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"approved_budget_usd":200}'
# 202 {"ok":true,"project":{...}}
| Method and path | Description |
|---|---|
GET /projects, POST /projects/draft, POST /projects/from-template | List and create drafts. Body: goal, template_id (required for from-template), params, budget_usd. |
GET /projects/{id}, /snapshot, /health, /approvals | Project state, health and approvals. |
PATCH /projects/{id}/plan, POST /projects/{id}/estimate, /validate, /launch | Edit, estimate, validate and launch. launch requires approved_budget_usd greater than 0. |
POST /projects/{id}/control, PUT /projects/{id}/budget | Pause, resume or cancel (action) and set the budget (budget_usd). |
POST /projects/{id}/nodes/{nodeId}/retry, /skip, /replan | Recover a failed or blocked node. |
GET/POST /project-templates, GET/PUT/DELETE /project-templates/{id} | Templates with versions. |
GET/POST /skills, GET/PUT /skills/{id} | Skill catalog and editing. |
POST /skills/{id}/submit, /review, /disable | Review workflow; review and disable need skills:review. |
GET /agents/{id}/skills, PUT/DELETE /agents/{id}/skills/{skillId} | Assign skills to an agent. |
Warning
Launching a project starts spending against approved_budget_usd. Check POST /projects/{id}/estimate before you launch.
Shop, rewards and drive#
Finished work pays coins into the organization's wallet. A purchase is idempotent: the same Idempotency-Key (or idempotency_key in the body) returns the original result without charging again. Without enough balance the response is 409 with insufficient_coins.
The drive stores the organization's files with versions, a trash and per-folder grants. An upload creates the file (201) or adds a version to it (200); with expected_version you get 409 version_conflict if someone changed it first.
curl http://localhost:8080/api/v1/rewards/wallet -H 'Authorization: Bearer <token>'
curl -X POST http://localhost:8080/api/v1/shop/purchases \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-H 'Idempotency-Key: <unique key>' \
-d '{"item_id":"<item id>"}'
# 201 first purchase; 200 when the same key is replayed
# Upload a file to the drive (raw body)
curl -X PUT 'http://localhost:8080/api/v1/drive/upload?path=/Shared/plan.xlsx' \
-H 'Authorization: Bearer <token>' --data-binary @plan.xlsx
# 201 created, 200 new version
| Method and path | Description |
|---|---|
GET /rewards/wallet, /rewards/ledger, /rewards/rules | Balance, ledger and payout rules. |
GET /shop/catalog, GET /shop/state | Catalog and state (inventory, equipped, office). |
POST /shop/purchases | Buy. Body: item_id, optional idempotency_key. Returns 201, or 200 on a replay. |
PUT/DELETE /agents/{id}/equipment/{slot} | Equip or remove a character item. Body: item_id. |
PUT/DELETE /office/items/{slot} | Place or remove an office item. |
GET /drive/list, /drive/search, /drive/usage, /drive/trash | Browse the drive. |
PUT /drive/upload?path= | Upload or version a file (raw body). Optional: message, expected_version. |
GET /drive/files/{id}, /content, /text, /preview, /versions, /diff | Read a file and its history. |
POST /drive/folders, POST /drive/files/{id}/move, DELETE /drive/files/{id} | Organize and delete (to the trash). |
Note
The drive has more routes: restoring versions and files, tags (PUT /drive/files/{id}/tags) and agent grants on folders (/drive/grants).
Connections, controls and settings#
Connections store encrypted credentials for external services. Controls let you pause agents, put the organization in read-only mode or trigger a kill switch; lifting it (POST /org/controls/release) is owner-only.
# Pause one agent (action: pause | resume | read_only | read_write)
curl -X POST http://localhost:8080/api/v1/agents/<id>/control \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"action":"pause","drain":"graceful","reason":"maintenance"}'
# Organization kill switch (level: freeze | lockdown)
curl -X POST http://localhost:8080/api/v1/org/controls/kill-switch \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"level":"freeze","reason":"incident"}'
| Method and path | Description |
|---|---|
GET /connection-providers, GET/POST /connections | Available providers and connections. |
PATCH /connections/{id}, PUT /connections/{id}/credential | Edit and store the credential (write-only). |
POST /connections/{id}/oauth/start, POST /connections/{id}/test | Start OAuth and test the connection. |
GET /connections/oauth/callback | Public OAuth callback, verified by a single-use state. |
GET/PUT /org/controls, POST /org/controls/kill-switch, /release | Organization mode (normal, read_only) and kill switch (freeze, lockdown). |
GET/PUT /org/operating-hours, /org/anomaly-settings | Operating hours and anomaly settings. |
GET/PUT /org/settings, GET/POST /onboarding | Organization settings and onboarding. |
GET /settings/llm, PUT/DELETE /settings/llm/providers/{provider} | Bring-your-own model credentials (owner or admin). |
GET/PUT /settings/model-policy, /settings/code-execution | Model policy and code execution (owner or admin). |
GET /audit, /audit/export, /audit/verify | Chained audit trail, export and verification (audit:read). |
Warning
Without CONNECTIONS_KEK the server cannot store real credentials: those routes answer 503 kek_missing and only simulated connections work. Bring-your-own model keys (/settings/llm) need it too.
Generic API sources#
Generic API sources: a custom_api connection with a source object turns any platform with an HTTP API and a key (an ERP, a CRM, an internal API) into something agents can query without writing an adapter. It only sends GET; if you define endpoints, only those can be called and only the listed parameters travel. Agents ask for it as api.get with connection_alias and get the result back as untrusted data. Every task tells the agent which sources it has been granted.
curl -X POST http://localhost:8080/api/v1/connections \n -H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \n -d '{"provider":"custom_api","kind":"api_key","label":"ERP","secret":"<api-key>",
"source":{"base_url":"https://erp.example.com/api",
"auth":{"style":"header","name":"X-API-Key"},
"description":"Inventory, orders and customers",
"endpoints":[{"name":"stock","path":"/products/{sku}/stock","params":["warehouse"]}]}}'
# What an agent asks for (the backend runs it; the agent never sees the key):
# {"tool":"api","action":"get","args":{"connection_alias":"erp","endpoint":"stock","params":{"sku":"A-1"}}}
WebSocket and health#
GET /ws opens your organization's event channel. On connect you receive hello with the agent list, then every domain event; an event with an audience reaches only the users it names. There are no topic subscriptions and no replay: after reconnecting, reload state over REST.
The server pings every 30 s and closes if no pong arrives within 60 s. From the client it only processes small presence frames (position, status) when that feature is on; everything else is ignored. With authentication on, the server closes the socket with code 4401 after 15 minutes (the access-token lifetime) and the client must reconnect with a fresh token.
GET /healthz and GET /api/v1/healthz need no authentication. They return status, runtime (up or down) and, when the runtime answers, runtime_mode (simulation or live).
# Browsers cannot set headers on a WebSocket: pass the token in the query string
wscat -c 'ws://localhost:8080/ws?access_token=<token>'
< {"id":"...","type":"hello","ts":"...","org_id":"<org id>","payload":{"agents":[...]}}
< {"id":"...","type":"agent.state_changed","ts":"...","org_id":"<org id>","agent_id":"sales","payload":{...}}
# Frame shape
{ "id": "<uuid>", "type": "<event.type>", "ts": "<RFC 3339>",
"org_id": "<org id>", "agent_id": "<optional>", "payload": { } }