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.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.
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.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_unavailableand the gateway keeps serving its pinned snapshot. Shedding is the point: answering200with 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 is404.