Skip to content
AI Workforce OS
ES EN
Create account

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.

9 min read

On this page
  1. Base URL and versioning
  2. Authentication
  3. API tokens for integrations
  4. Errors, limits and conventions
  5. Requests, chat, tasks and reports
  6. Approvals and agents
  7. Projects and skills
  8. Shop, rewards and drive
  9. Connections, controls and settings
  10. Generic API sources
  11. WebSocket and health
Back to top

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.

Base URL and healthBash
# 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.

Login and authenticated callsBash
# 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 pathDescription
POST /auth/registerCreates 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/loginSigns in. Body: email, password, optional org_id.
POST /auth/mfa/verifyCompletes a login that answered mfa_required. Body: mfa_token, code.
POST /auth/refreshRotates the refresh token (cookie + X-WFAI-CSRF) and returns a new access token.
POST /auth/logoutEnds the current session and clears the cookie. Returns 204.
POST /auth/logout-allRevokes all of the user's sessions. Returns 204.
POST /auth/switch-orgOpens a session in another of the user's organizations. Body: org_id.
GET /auth/me, GET /auth/configUser, 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: scope agents:read.
  • GET /requests, GET /requests/{id}: scope requests:read (also covered by requests:write).
  • POST /requests: scope requests:write. Same body as the session API (text, budget_cap_usd); answers 202 with request_id.
  • GET /reports, GET /reports/{id}: scope reports:read.
  • GET /usage: scope usage:read.
Create and use a tokenBash
# 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 /activity accepts limit (default 100, maximum 1000).
  • GET /conversations/{id}/messages accepts limit (maximum 200) and before, and returns the X-Has-More and X-Next-Before headers.
  • GET /conversations accepts limit and offset and returns X-Has-More and X-Next-Offset.
  • GET /rewards/ledger accepts limit (default 50, maximum 200) and before; GET /agents/{id}/memory accepts limit and q.
  • 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.
Error shapesJSON
// 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": "..." } } }
CodeMeaning
400Invalid body or parameters.
401Missing, invalid or expired token.
403The role lacks the permission, or the CSRF check failed.
404The resource does not exist in your organization.
409State conflict (for example, a different expected version or insufficient coins).
413Body too large.
422Validation failed on the /auth/* routes; fields says which field failed.
429Rate limit, or account locked after repeated failed logins.
503Feature 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 and a chat messageBash
# 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
GET /api/v1/requests/{id}JSON
{
  "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 pathDescription
POST /requestsCreates 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}/estimateCost breakdown and upfront estimate.
POST /requests/{id}/confirmConfirms or cancels an estimate. Body: decision (proceed or cancel), optional budget_cap_usd.
PUT /requests/{id}/budgetChanges the cap (budget_cap_usd; 0 removes it). Admin or owner only.
POST /messagesChat message. Body: conversation (office by default, or agent:<id>), text, attachments (drive paths, up to 6). Returns 202.
GET /conversations, GET /conversations/{id}/messagesConversations and their messages.
POST /conversations/{id}/messagesPosts 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}/reassignIntervene in a running task. Admin or owner only.
GET /reports, GET /reports/{id}Final reports.
GET /activity, GET /metrics, GET /budgetActivity, 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 and decide approvalsBash
# 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 pathDescription
GET /approvalsList; status filter (pending, approved, rejected).
POST /approvals/{id}/decisionBody: decision (approve or reject), note, scope (once, session, project), always_allow, always_allow_any_agent, always_allow_days.
POST /approvals/batchDecides several approvals of a project. Body: filter (project_id, action), decision, expected_count, include_high.
GET /agents, GET /agents/{id}, GET /agents/{id}/detailAgents: state, tools, permissions and autonomy.
GET/PUT /agents/{id}/profile, GET /agents/{id}/tracesVersioned 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-templateHire an agent from a role template.
PUT /agents/{id}/budget, POST /agents/{id}/controlMonthly 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 and launch a projectBash
# 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 pathDescription
GET /projects, POST /projects/draft, POST /projects/from-templateList and create drafts. Body: goal, template_id (required for from-template), params, budget_usd.
GET /projects/{id}, /snapshot, /health, /approvalsProject state, health and approvals.
PATCH /projects/{id}/plan, POST /projects/{id}/estimate, /validate, /launchEdit, estimate, validate and launch. launch requires approved_budget_usd greater than 0.
POST /projects/{id}/control, PUT /projects/{id}/budgetPause, resume or cancel (action) and set the budget (budget_usd).
POST /projects/{id}/nodes/{nodeId}/retry, /skip, /replanRecover 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, /disableReview 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.

Wallet, purchase and uploadBash
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 pathDescription
GET /rewards/wallet, /rewards/ledger, /rewards/rulesBalance, ledger and payout rules.
GET /shop/catalog, GET /shop/stateCatalog and state (inventory, equipped, office).
POST /shop/purchasesBuy. 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/trashBrowse 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, /diffRead 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 an agent and trigger the kill switchBash
# 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 pathDescription
GET /connection-providers, GET/POST /connectionsAvailable providers and connections.
PATCH /connections/{id}, PUT /connections/{id}/credentialEdit and store the credential (write-only).
POST /connections/{id}/oauth/start, POST /connections/{id}/testStart OAuth and test the connection.
GET /connections/oauth/callbackPublic OAuth callback, verified by a single-use state.
GET/PUT /org/controls, POST /org/controls/kill-switch, /releaseOrganization mode (normal, read_only) and kill switch (freeze, lockdown).
GET/PUT /org/operating-hours, /org/anomaly-settingsOperating hours and anomaly settings.
GET/PUT /org/settings, GET/POST /onboardingOrganization 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-executionModel policy and code execution (owner or admin).
GET /audit, /audit/export, /audit/verifyChained 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.

Create a read-only API sourceBash
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).

Connect to the WebSocketBash
# 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": { } }