Skip to content

REST API reference

This covers the token-authenticated Agent API (/api/v1/*) meant for external integrations. The React SPA talks to a much larger set of session-authenticated routes (admin, boards, ideas, comments, account, …) that are internal to the app and not a stable public contract — those aren’t documented here; read src/Http/AppFactory.php and the corresponding src/Http/Action/* class if you need to understand a specific one.

Authentication

Every Agent API route requires:

Authorization: Bearer <token>

Quick check that a token works:

curl -s https://<your-install>/api/v1/board \
  -H "Authorization: Bearer <token>"

Tokens are created and revoked in the admin UI at the account level (GET/POST /admin/tokens, POST /admin/tokens/{id}/revoke, session-authenticated, requires account-admin role). A token grants a settable SET of the account’s boards, plus a coarse read/write scope — it can never read or write a board outside that set, regardless of what the request asks for, and a read-scoped token can never call the one write endpoint (POST /api/v1/ideas / the create_idea MCP tool). The plaintext token is shown exactly once, at creation time; only its SHA-256 hash is stored server-side, compared with a constant-time comparison. Creating a new token is gated by the injected PlanPolicy::agentApiAllowed() (Community’s default, UnrestrictedPlanPolicy, always allows it); an invalid/missing bearer token, a token without write scope on a write request, or a token for a board outside its grant, results in 401/403.

A token that grants exactly one board resolves it automatically. A token that grants more than one board requires an explicit ?board=<slug> query parameter on every request — naming a board the token isn’t granted returns 403 board_not_granted; omitting it on a multi-board token returns 400 board_required.

Agent API requests bypass session cookies and CSRF entirely — they are a separate trust boundary (see architecture.md).

Rate limits

Two buckets, keyed per token (config.phprate_limits, see configuration.md):

  • apitoken:read — 120/minute default. Covers GET /api/v1/board, GET /api/v1/ideas, GET /api/v1/ideas/{id}.
  • apitoken:write — 20/hour default. Covers POST /api/v1/ideas.

The MCP endpoint (POST /api/v1/mcp, see mcp-server.md) reuses the same buckets and the same per-token key — a token’s REST usage and MCP usage share one budget, not two independent ones.

Endpoints

Every endpoint below resolves its board as described above (?board=<slug> when the token grants more than one).

GET /api/v1/board

Returns the resolved board.

GET /api/v1/ideas

List ideas on the resolved board. Query parameters:

ParamTypeNotes
statusstringoptional, filter by status
sortstringoptional
pageintoptional, ≥ 1

GET /api/v1/ideas/{id}

Fetch a single idea by numeric ID ({id:[0-9]+}). Returns 404 if the idea doesn’t belong to the resolved board.

POST /api/v1/ideas

Create a new idea on the resolved board. Requires a write-scoped token — a read-scoped token gets 403 insufficient_scope.

FieldTypeConstraint
titlestringrequired
bodystringrequired
curl -s https://<your-install>/api/v1/ideas \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"title": "Dark mode", "body": "Would love a dark theme for the board."}'

Consumes the apitoken:write rate-limit bucket.

Errors

Standard HTTP status codes: 401 (missing/invalid token), 403 (token valid but not authorized for the requested board/action, or Agent API not available on the current plan), 404 (resource not found or not on this token’s board — the API deliberately does not distinguish “doesn’t exist” from “exists on another board”, to avoid leaking cross-board existence), 422 (validation failure), 429 (rate limit exceeded). Response bodies are JSON; exact error shapes are not yet part of a versioned contract — treat the HTTP status code as authoritative.

Other trust boundaries

One more authenticated surface exists outside this Agent API, documented for completeness but not part of the “external integration” contract above:

  • Operator panel (/operator/*) — platform-wide, session-authenticated, requires the operator authorization level (users.is_operator, settable only via direct database access, no signup path grants it). Relevant only to whoever runs the installation, not to external integrators.

Extensions registered in config.php may add routes of their own (with their own authentication); those are documented by the extension, not here.