Introduction

OverviewArchitectureAgent Experience

The product

This repository

Structure

Usage

Other

Interfaces

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: true

This 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=25

Responses 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.

ParameterEffect
?view=compactThe token-lean projection. This is the default.
?view=fullThe 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/stream

Server-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.