REST API
All endpoints require Authorization: Bearer <token> unless noted. Responses are JSON. Endpoints under /auth/* and /oauth/* live on the klaxon-auth binary (port 3001 by default); everything else lives on klaxon-server (port 3000). In production the Ingress path-routes them to the right pod — from a client's perspective they share one origin.
Every write endpoint under /api/admin/* requires a specific permission string (see Roles & Permissions). A missing permission returns 403 with {"error":"permission denied","required":"..."}.
Health & Ops
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health | No | Returns "ok" |
| GET | /ready | No | Checks DB connection (503 if down) |
| GET | /metrics | No | Prometheus metrics |
Auth
The auth binary exposes three classes of endpoint: IdP login flows, OAuth 2.1 Authorization Server, and the account-management surface the UI calls once a session exists.
IdP login
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /auth/oauth/github | No | Start GitHub OAuth flow (redirects) |
| GET | /auth/oauth/github/callback | No | GitHub OAuth callback |
| GET | /auth/oauth/google | No | Start Google OAuth flow (redirects) |
| GET | /auth/oauth/google/callback | No | Google OAuth callback |
| GET | /auth/oauth/apple | No | Start Sign in with Apple flow (redirects) |
| POST | /auth/oauth/apple/callback | No | Apple OAuth callback (POST form per Apple spec) |
| POST | /auth/magic-link | No | Request magic link login email |
| GET | /auth/verify | No | Exchange magic link token for session |
See Authentication for the full IdP setup + env vars for each provider.
OAuth 2.1 Authorization Server
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /.well-known/oauth-authorization-server | No | RFC 8414 metadata discovery |
| GET | /.well-known/oauth-protected-resource | No | RFC 9728 resource metadata |
| POST | /oauth/register | No | RFC 7591 dynamic client registration |
| GET | /oauth/authorize | Session | Browser authorization endpoint + consent |
| POST | /oauth/token | No | Token exchange (PKCE S256 required) + refresh |
| POST | /oauth/revoke | No | RFC 7009 token revocation |
Account & sessions
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/logout | Yes | Revoke the current session |
| GET | /auth/me | Yes | Return authenticated identity |
| POST | /auth/orgs | Yes | Create a new organization |
| POST | /auth/switch-org | Yes | Mint a new session scoped to a different org the user belongs to |
| GET | /auth/api-keys | Yes | List this user's API keys |
| POST | /auth/api-keys | Yes | Create API key (auto-creates agent) |
| POST | /auth/api-keys/revoke | Yes | Soft-revoke an API key |
Magic Link — Request
curl -X POST http://localhost:3000/auth/magic-link \
-H "Content-Type: application/json" \
-d '{"email": "alice@example.com", "org_id": "ORG_UUID"}'Response (always 200 — doesn't leak user existence):
{ "message": "If an account exists, a login link has been sent." }In dev mode (no SMTP configured), the response includes the token:
{ "message": "Check your email (dev: token in response)", "token": "base64url-token" }Magic Link — Verify
curl "http://localhost:3000/auth/verify?token=base64url-token-from-email"Response:
{ "token": "session-bearer-token", "user_id": "...", "org_id": "..." }Items
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/items | Yes | Create item |
| GET | /api/items | Yes | List items (cursor-paginated) |
| GET | /api/items/:id | Yes | Get item by ID |
| PATCH | /api/items/:id | Yes | Update item fields |
| POST | /api/items/:id/answer | Yes | Submit form response (validates against schema) |
| POST | /api/items/:id/ack | Yes | Mark as viewed |
| POST | /api/items/:id/dismiss | Yes | Dismiss |
| POST | /api/items/:id/archive | Yes | Archive |
| POST | /api/items/:id/unarchive | Yes | Unarchive |
| POST | /api/items/:id/restore | Yes | Restore dismissed/expired/withdrawn |
| POST | /api/items/:id/snooze | Yes | Snooze until datetime |
| POST | /api/items/:id/unsnooze | Yes | Remove snooze |
| POST | /api/items/:id/pin | Yes | Pin to top |
| POST | /api/items/:id/unpin | Yes | Unpin |
List Items — Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
limit | int | 50 | Max items (capped at 200) |
cursor | string | — | Pagination cursor from previous response |
status | string | — | Filter by status (open, answered, etc.) |
channel_id | UUID | — | Filter by channel |
include_archived | bool | false | Include archived items |
q | string | — | Full-text search on title + message |
List Response
{
"items": [...],
"next_cursor": "base64-encoded-cursor-or-null"
}Create Item — Body
{
"title": "Deploy failed",
"message": "Error in stage 3",
"level": "error",
"priority": 3,
"channel_id": "uuid",
"tags": ["deploy", "prod"],
"form": { ... },
"metadata": { "commit": "abc123" },
"callback_url": "https://n8n.example.com/webhook/abc123",
"deadline_at": "2026-04-15T12:00:00Z"
}Only title is required. The callback_url causes the server to POST the response back to that URL when the item is answered/dismissed/acked (see Integrations).
Answer Item — Body
{ "values": { "field_id": "value", ... } }Returns 422 with field-level errors if validation fails:
{ "error": "validation failed", "fields": { "name": "field 'name' is required" } }Timeline
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/items/:id/timeline | Yes | Merged audit events + comments (chronological) |
Returns a unified feed:
[
{ "entry_type": "event", "action": "item.create", "actor_type": "agent", "created_at": "..." },
{ "entry_type": "comment", "body": "investigating", "actor_type": "user", "created_at": "..." }
]Comments
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/items/:id/comments | Yes | List comments (chronological) |
| POST | /api/items/:id/comments | Yes | Add comment |
Add Comment — Body
{ "body": "Looks good to me" }Attachments
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/items/:id/attachments | Yes | List attachment metadata |
| POST | /api/items/:id/attachments | Yes | Upload attachment (base64) |
| GET | /api/items/:id/attachments/:aid/download | Yes | Download file |
| DELETE | /api/items/:id/attachments/:aid | Yes | Delete attachment + blob |
Upload Attachment — Body
{
"filename": "report.html",
"content_type": "text/html",
"data": "base64-encoded-file-content"
}Channels
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/channels | Yes | List channels |
| POST | /api/channels | Yes | Create channel |
Create Channel — Body
{ "name": "deploys", "mode": "open" }Templates
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/templates | Yes | List templates |
| POST | /api/templates | Yes | Create/upsert template |
| DELETE | /api/templates/:name | Yes | Delete template |
Webhooks
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/webhooks | Yes | List org webhooks |
| POST | /api/webhooks | Yes | Create webhook |
| DELETE | /api/webhooks/:id | Yes | Delete webhook |
| POST | /api/webhooks/inbound/:token | No | Inbound: external services create items |
Create Webhook — Body
{
"name": "slack-notifications",
"url": "https://hooks.slack.com/...",
"events": ["item.created", "item.answered"],
"secret": "optional-hmac-secret"
}Outbound deliveries include X-Klaxon-Signature: sha256=... and X-Klaxon-Event headers.
Inbound Webhook
See Integrations for the full inbound webhook format including form and callback_url.
Audit
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/audit | Yes | List audit entries (cursor-paginated) |
Query params: limit, cursor, item_id (filter by item).
Settings
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/settings | Yes | List org settings (key-value) |
| PUT | /api/settings/:key | Yes | Upsert a setting |
Set Setting — Body
{ "value": true }Stats
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/stats | Yes | Aggregate counts by status, level, channel |
Response:
{
"total": 150,
"open": 23,
"answered": 89,
"dismissed": 30,
"archived": 8,
"by_level": { "info": 100, "warning": 30, "error": 20 },
"by_channel": { "deploys": 50, "alerts": 40, "(none)": 60 }
}Push
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/push/register | Yes | Register FCM device token |
| POST | /api/push/unregister | Yes | Remove device token |
Real-time
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/ws | Yes | WebSocket (clients) |
| GET | /mcp/sse | Yes | SSE (MCP agents) |
| POST | /mcp | Yes | MCP JSON-RPC 2.0 |
Admin — Members
All under /api/admin/*. See Admin Panel.
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /api/admin/users | users.invite | List org members |
| POST | /api/admin/users/:user_id/role | users.change_role | Change a member's role |
| POST | /api/admin/users/:user_id/deactivate | users.remove | Remove a member |
| GET | /api/admin/org | org.manage | Fetch org details |
| PATCH | /api/admin/org | org.manage | Update org name / slug / metadata |
| GET | /api/admin/usage | org.manage | Current plan + usage vs. limits |
| POST | /api/admin/agents | agents.manage | Create an API agent (gates on max_agents) |
Demoting or removing the only Owner returns 409 Conflict.
Admin — Teams
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /api/admin/teams | teams.manage_members | List teams |
| POST | /api/admin/teams | teams.create | Create team (max_teams gate) |
| PATCH | /api/admin/teams/:team_id | teams.manage_members | Rename / re-slug |
| DELETE | /api/admin/teams/:team_id | teams.delete | Delete team |
| GET | /api/admin/teams/:team_id/members | teams.manage_members | List team members |
| POST | /api/admin/teams/:team_id/members | teams.manage_members | Add member |
| DELETE | /api/admin/teams/:team_id/members/:user_id | teams.manage_members | Remove member |
Admin — Roles
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /api/admin/roles | users.invite | List system + org-local roles |
| POST | /api/admin/roles | users.change_role | Create a custom role (escalation-gated) |
| PATCH | /api/admin/roles/:role_id | users.change_role | Edit a custom role (system roles read-only) |
| DELETE | /api/admin/roles/:role_id | users.change_role | Delete a custom role (fails if members assigned) |
Admin — Invitations
Server-side admin routes:
| Method | Path | Permission | Description |
|---|---|---|---|
| POST | /api/admin/invitations | users.invite | Create email or link invite |
| GET | /api/admin/invitations | users.invite | List active invitations |
| POST | /api/admin/invitations/:id/revoke | users.invite | Revoke |
Public routes for invitees (served by klaxon-auth):
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /auth/invitations/:code | No | Preview (safe to show before sign-in) |
| POST | /auth/invitations/accept | Session | Accept; returns fresh token scoped to org |
Accept errors: 403 wrong email, 410 revoked/expired/fully used, 402 plan_limit_exceeded (key max_seats).
Billing
All owner-gated (org.billing).
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/billing/subscription | Yes | Current plan, status, cancel-at-period-end |
| POST | /api/billing/checkout | Yes | Returns a Stripe Checkout URL |
| POST | /api/billing/portal | Yes | Returns a Stripe Billing Portal URL |
| POST | /webhooks/stripe | HMAC | Stripe webhook (verified via STRIPE_WEBHOOK_SECRET) |
Plan-limit gate fires a 402 Payment Required on overage — see Billing.