Everything in this router is scoped by control’s own membership record (ADR-0033). The browser supplies a session token and a workspace id, and neither is trusted until a membership row has matched them. Role actions are enforced by control and never delegated to the identity provider (ADR-0037).

Authentication and roles

Authorization: Bearer <session token>, verified as /api-reference/overview describes. A missing or malformed header is 401 session required; an unconfigured identity provider is 503 dashboard identity is not configured. A caller who is not a member of the workspace gets 404 workspace not found, never 403 — a distinguishable “exists but forbidden” would be an oracle for workspace ids across tenants. A member whose role does not grant the action gets 403 action not permitted. A frozen workspace answers 409 workspace is frozen on any action that is not a read. The three read actions are the only ones a frozen workspace still grants.

Get the current operator

GET /api/v1/me The signed-in operator and the workspaces they may act in. This is the one endpoint that writes as a side effect of a read: it provisions a workspace on first sign-in so the dashboard has somewhere to land, and publishes that workspace’s first snapshot after the commit. Idempotent — a second call returns the same workspace.
object
{id, email}.
array
{id, org_id, slug, env, frozen, role} for every membership.
string
The workspace to land on.

Workspace overview

GET /api/v1/workspaces/{workspace_id}/overview Counts the first dashboard screen is allowed to state. Every number is one Obol has measured; an empty workspace reports zeroes rather than a projection.
object
{id, org_id, slug, env, frozen, snapshot_version}.
object
{active, total}. active counts keys whose effective status is active right now, which accounts for expiry and revocation.
object
{total, connections_active}. Model-provider connectors are excluded.
object
{published, revision_id, published_at, bundle_hash}.
object
{total, last_at}.

Client endpoints

GET /api/v1/workspaces/{workspace_id}/mcp Addresses an operator pastes into an IDE. Addresses only — the key plaintext exists in the browser for exactly one render and is never stored, echoed, or re-served (ADR-0004), so control cannot fill it in and does not pretend to.
string
The gateway base URL.
string
{gateway_url}/mcp.
string
{gateway_url}/v1.
string
ob_live in a prod workspace, ob_test otherwise.
Also returned: workspace_id and env.

List receipts

GET /api/v1/workspaces/{workspace_id}/receipts The latest revision of each invocation’s receipt, newest first. Superseded revisions are filtered out before the limit is applied, so a page is never short because a receipt was revised.
integer
default:"50"
1–200.
string
Opaque cursor, at most 2048 characters. A v1./v2. value pages on (occurred_at, receipt_id); anything else is read as a time cursor. Cannot be combined with before.
string
Opaque receipt cursor, at most 1024 characters. A legacy RFC3339 timestamp is still accepted as a bound. Newly issued cursors always carry the full timestamp and receipt id pair so ties inside a second can be traversed (ADR-0042).
string
One of awaiting_approval, rate_limited, not_ready, denied, in_flight, completed, inconclusive. An unknown value is 400 unknown invocation state — a filter that quietly does nothing would show a complete-looking list that is not the list you asked for.
string
One of gateway_observed, vendor_signed, connector_attested, broker_attested, untrusted. Unknown values are 400 unknown evidence trust class.
string
One of not_required, pending, verified, contradicted, inconclusive. Unknown values are 400 unknown verification state.
string
One of allow, deny, approval_required. Unknown values are 422 unknown authorization decision.
string
Exact tool name, 1–256 characters.
string
Exact virtual key id, 1–64 characters.
string
Exact connection id, 1–64 characters.
string
Start of a UTC window, at most 64 characters. Must be sent with to, or the request is 422 from and to must be provided together. A malformed window is 422 invalid UTC window.
string
End of the UTC window, exclusive.

Response

array
List-shaped receipt projections: receipt_id, receipt_revision, invocation_id, tool, key_id, connection_id, state, decision, dispatch_state, evidence_trust, verification_state, verification_method, effect_claim, verification_evidence_trust, vendor_http_status, upstream_id, latency_ms, usd_micros, attempt_count, occurred_at, updated_at.
string
Cursor for the next page, or null.
string
The same value, under the older field name. Both are emitted so existing clients keep working.
evidence_trust names the class of proof, and the class is tier-dependent. A native route can be gateway_observed; a federated catalog route is broker_attested or untrusted and can never be verified. Read a receipt as “what this call achieved, and how much its evidence is worth” — see /receipts/evidence.
The remaining closed vocabularies, from packages/proto/receipt.schema.json: dispatch_state is not_attempted, accepted, rejected, or unknown; verification_method is staged_plan, none, or response_binding; and effect_claim is accepted, persisted, or delivered, or null when the envelope carries no verification block. A 202 is accepted, never persisted.

Get a receipt

GET /api/v1/workspaces/{workspace_id}/receipts/{receipt_id}
string
required
Any receipt id belonging to the invocation. The endpoint resolves the id to its invocation_id and then returns the newest revision, so an older id still reaches the current receipt.
integer
Minimum 1. Returns that immutable historical revision instead of the newest one. 404 receipt not found when the revision does not exist.
Returns the list projection plus idempotency_key, request_id, policy_id, policy_revision_id, and envelope. The envelope ships whole — the detail view has to be able to say what the gateway recorded, including per-candidate routing scores and weights, rather than what control found convenient to model. 404 receipt not found when the id is not in this workspace.

List receipt revisions

GET /api/v1/workspaces/{workspace_id}/receipts/{receipt_id}/revisions Immutable historical revisions of one invocation’s receipt, newest revision first.
integer
default:"50"
1–200.
integer
Minimum 1. Returns revisions strictly below this number.
array
List-shaped projections of each historical envelope.
integer
Pass as before_revision for the next page, or null on the last page.

Adjacent surfaces

Two routers sit outside /api/v1 and are not part of the operator API.

Health

GET /healthz Unauthenticated liveness. Returns {"ok": true}. Deliberately exempt from the Cache-Control: no-store middleware that covers /api/v1.

Internal service reads

GET /internal/v1/workspaces/{workspace_id}/snapshot and GET /internal/v1/keys/{key_hash} are the gateway’s own reads. They are authenticated by a service JWT in the Authorization header, not a dashboard session, and they are not a public API — treat them as the gateway/control wire contract.
  • The snapshot read recompiles the committed workspace and policy pair. When it cannot — a busy compile seam, a bundle that no longer validates, a stored route qualifier that will not deserialize — it answers 503 snapshot_unavailable and the gateway keeps serving its pinned snapshot. Shedding is the point: answering 200 with a body the gateway cannot load would take the tenant dark.
  • The key read takes a lowercase 64-character SHA-256 hex digest. Anything else is 400 key hash must be sha256 hex; an unknown digest or a missing workspace is 404.
See /gateway/overview for how the data plane consumes both.