REST API
The planned HTTP door — versioned paths, a capability manifest, cursor pagination, idempotency and RFC 9457 errors
Not built yet. There is no /api/v1 in this repository. apps/api currently serves a health check,
a keep-alive cron route, and Clerk and Stripe webhook handlers. Every path below is a proposed
shape, taken from spec/02-agent-interfaces.md.
REST is the lowest-friction door: curl, webhooks, shell scripts, any runtime, no client library. That is its job. It is not the "real" interface with the others layered on top — all four doors are planned to sit on one service layer that owns validation, the task state machine and event writes.
Planned location: apps/api.
Versioning
The base path is /api/v1. The version is in the path, and a breaking change mints v2 rather than
altering v1 in place. An agent that hardcoded a path keeps working, which is the only guarantee
that makes hardcoding a path reasonable.
Discovery
Two endpoints exist so an agent never has to guess what the API can do.
GET /api/v1 returns a capability manifest: the version, the auth mode, the rate limits, an index
of entities, and a link to the OpenAPI document.
GET /api/v1/openapi.json returns the OpenAPI document, generated from the same Zod schemas the
handlers validate with. Because it is generated rather than maintained, it cannot drift from the
behaviour it describes.
Authentication and scopes
Authorization: Bearer <AgentToken>Scopes are enforced per route. Tokens are scoped per agent and per machine, hashed at rest, and revocable one at a time. Every request is logged with the agent id that made it.
Placeholders only
Every token in these docs is an obvious placeholder. Real tokens are returned once at creation and are never readable afterwards, so a real one has no reason to appear in a document.
Idempotency
Writes require an Idempotency-Key. A replay returns the original response, marked:
Idempotency-Replayed: trueThis is what makes a retry safe for a runner that lost its connection mid-write and cannot tell whether the first attempt landed.
Optimistic concurrency
Reads carry an ETag. Writes may pass If-Match.
GET /api/v1/tasks/tsk_01J...
ETag: "7"
PATCH /api/v1/tasks/tsk_01J...
If-Match: "7"A mismatch is 409 conflict_version. Two agents editing the same task cannot silently clobber each
other; the loser is told it lost and can re-read and retry.
Pagination
Opaque cursors, never offsets.
GET /api/v1/tasks?status=todo&cursor=...&limit=25Responses carry has_more and next_cursor. The default limit is 25 and the maximum is 100. No
response returns an unbounded array, because an unbounded array is an unbounded context cost.
Token-lean responses
Default responses are the compact projection. Fuller objects are opt-in.
| Parameter | Effect |
|---|---|
?view=compact | The token-lean projection. This is the default. |
?view=full | The complete object. |
?fields= | Selects specific columns. |
?since= | Returns the delta rather than the whole list. |
Errors
RFC 9457 application/problem+json, on every failure:
{
"type": "https://opb.brain/errors/invalid_transition",
"title": "Illegal status transition",
"status": 422,
"code": "invalid_transition",
"detail": "Task tsk_01J is 'in_progress'; 'done' is not reachable from there.",
"hint": "Open a PR and move to 'in_review' first.",
"legal_transitions": ["in_review", "blocked", "cancelled"],
"retryable": false,
"docs_url": "https://opb.brain/spec/domain-model#task-state-machine"
}The code is stable and machine-readable. retryable appears on every error. hint says what to
do next. An agent must never have to string-match a human-readable message to decide its next call.
Never a bare 500 with a stack trace, and never a 200 carrying an error in the body.
Realtime
GET /api/v1/events/streamServer-sent events, taking the same since cursor as the event timeline, so a runner that
reconnects resumes rather than restarts.
Outbound webhooks are the other path: a WebhookSubscription holds a URL, an event filter, a
signing secret, and a delivery log with retries. It exists so a remote runner can be woken instead
of polling.
Polling is a supported fallback. It is not the design.
Rate limits
Limits are per token. A rejection is 429 with Retry-After, and every response carries the
remaining budget, so a well-behaved runner can slow down before it is refused rather than after.