pending, active, and disabled.
Brokers are vendors, never authority. A federated connection uses your own
OAuth client, a broker never mints a receipt or assigns evidence trust, and
broker-side retries are disabled on every call (invariants 8 and 9).
Authentication
All routes requireAuthorization: Bearer <session token>
(/api-reference/overview) and carry
Cache-Control: no-store. Reads need workspace.read; every mutation needs
connection.manage, which is granted to admin and owner. A non-member gets
404 workspace not found. Errors raised inside the connection service surface
as {"detail": "..."} with the status noted per endpoint; a failed gateway call
surfaces as 502 {"error": {"code": "upstream_error", ...}} and an unavailable
hot path as 503 {"error": {"code": "not_ready", ...}}.
Read the catalog
GET /api/v1/workspaces/{workspace_id}/catalog
Two shapes from one endpoint, discriminated by the presence of a page object
in the response. With no query parameters you get every catalog row verbatim.
Supply any of q, category, source, limit, offset, or cursor and you
get one page of gallery cards instead.
string
Search text, at most 64 characters. Longer is refused rather than truncated
into a different search.
string
Category token, at most 64 characters. A well-formed token nobody uses is an
empty page, not an error.
string
Lane:
native, nango, or composio. A closed enum — anything else is
422 unknown catalog source. The lane also joins the cursor fingerprint, so a
cursor issued in one lane cannot be replayed in another.integer
default:"24"
1–100.
integer
0–100000. Cannot be combined with
cursor.string
At most 128 characters, from a previous
page.next_cursor.Unfiltered response
integer
Schema version of the frozen index.
string
When the index was frozen.
array
Every catalog row verbatim —
slug, display_name, description,
categories, default_tier, and sources (each with provider,
auth_modes, mcp, allowed_mcp_origins, and upstream_id) — plus a
connections array holding this workspace’s connections on that slug.Browse response
The sameschema_version and generated_at, plus:
array
Gallery cards:
slug, display_name, description, categories,
default_tier, and sources reduced to {provider, auth_modes}, plus
connections. The card projection drops upstream_id, mcp, and the
reviewed allowed_mcp_origins allowlist — the allowlist is what a federated
completion is checked against, so it is control’s input and never a browser’s.array
{name, count} facets over the unfiltered catalog, so the rail does not
reshuffle while an operator types. Sorted by descending count, then name.array
{source, count}, one row per lane in native, nango, composio order. A
lane is never omitted, and its count may be 0.integer
How many rows more than one broker carries. The lanes overlap and are never a
partition, so a rail drawn from the counts alone would overstate coverage.
object
{total, limit, offset, next_cursor}. next_cursor is null on the last
page. An offset past the end is an empty page with the real total, not a
404.Browse errors
Read one catalog entry
GET /api/v1/workspaces/{workspace_id}/catalog/{slug}
The verbatim catalog row for one slug, with no connections attached.
404 unknown catalog slug when the slug is not in the index.
List connections
GET /api/v1/workspaces/{workspace_id}/connections
Every non-model connection in the workspace.
array
operator_connection projections.string
federated when custody is federated, trusted_worker when the target is a
worker, otherwise native.string
Derived from the tier:
gateway_observed for native, connector_attested for
a trusted worker, untrusted for federated.object
{catalog_session_id, expires_at} for a federated handoff this caller may
still finish, or null. It is returned only to a caller who holds
connection.manage, only for a session they themselves initiated, only while
the session is unconsumed and unexpired, only while the session’s provider
still matches the connection’s broker, and only while a published version
covers the workspace’s current snapshot version. Anything narrower at
completion time would be a handle that completion refuses.schema_version, connection_id, workspace_id,
connector_slug, connector_display_name, source, status, custody,
last4, expires_at, last_refreshed_at, refreshable,
credential_revision, and publication.
Register a catalog broker
POST /api/v1/workspaces/{workspace_id}/catalog-providers
Stores the workspace’s own broker API key so federated connect sessions can be
requested. The key is sent to the gateway to be sealed and only the ciphertext
is stored.
string
required
nango or composio.string
required
Your broker API key. At most 8192 bytes, no control characters.
200 with the mutation envelope. When the first snapshot publication
has not settled the response comes back with the connection still pending and
next.kind of none; retry once publication reports published.
Start a connection
POST /api/v1/workspaces/{workspace_id}/connections
string
required
Catalog slug.
404 unknown catalog slug if the index does not carry it.string
required
native, nango, or composio. 400 catalog source is not available when
the slug does not publish that lane.string
OAuth client id for the native OAuth path.
string
OAuth client secret for the native OAuth path, sealed before storage.
next:
A workspace holds at most one live connection per connector. Starting a second
one is
409 connector already has a live connection; an unfinished pending
shell on the same connector and custody is reused rather than duplicated.
Reconnect a connection
POST /api/v1/workspaces/{workspace_id}/connections/{connection_id}/reconnect
Resumes an unfinished setup. The connection must be pending; anything else is
409 connection is not pending.
string
Replacement OAuth client secret. Refused with
400 connection does not accept an oauth client secret when the connection has no OAuth profile.string
Replacement API key. Refused with
400 connection does not accept an api key
when the connection has no API-key profile. Full live keys are refused —
400 full live keys are refused; use a restricted key for anything starting
with sk_live_.400 provide only one credential. An empty body takes the mode
the connection already has: a federated connection gets a fresh
catalog_redirect — and any superseded unconsumed session is deleted first — and
a native OAuth connection gets a fresh redirect with its credential_revision
bumped and its outstanding OAuth state rows cleared.
Disable a connection
POST /api/v1/workspaces/{workspace_id}/connections/{connection_id}/disable
Keeps the connection and its sealed credential, but stops it serving: a disabled
connection is skipped when tools are published, and reconnecting restores it.
This is the reversible half of the duplicate-connection fix. Two active
connections on one connector publish two tool snapshots with the same name, and
the gateway binds a call to whichever sorts first — so the second credential
would be silently unreachable.
Idempotent: disabling an already-disabled connection answers with the projection
rather than a conflict.
Discard a connection
DELETE /api/v1/workspaces/{workspace_id}/connections/{connection_id}
204 with no body. The only deletion this plane offers, and only for a
connection that never held a credential — credential_revision still 0, no
sealed ciphertext, no refresh ciphertext, and no last4. Anything that has held
one, including a row whose secret was destroyed by purge-credential, is
409 connection has held a credential and is kept as a record.
The act stays on the record: connection.start was audited when the row was
made, and this adds connection.discard.
Purge a credential
POST /api/v1/workspaces/{workspace_id}/connections/{connection_id}/purge-credential
Destroys the sealed vendor credential and keeps the row that held it. Clears the
access and refresh envelopes, kms_key_id, kek_provider, cred_scheme,
last4, and expires_at; bumps credential_revision; and sets status to
disabled. The cleared last4 moves into the audit detail so the surviving
record can say which credential went.
Irreversible — Obol holds one copy of a vaulted secret and the plaintext never
left the gateway process. The connection returns to service only by being
re-credentialed. Idempotent when there is nothing to destroy and the connection
is already disabled.
Store a restricted key
POST /api/v1/workspaces/{workspace_id}/connections/{connection_id}/restricted-key
string
required
The restricted vendor key. Sealed by the gateway; control stores only the
envelope and
last4.api_key, otherwise
400 connection is not an api_key profile. Full live keys are refused with
400 full live keys are refused; use a restricted key — no pooled vendor
accounts and no full sk_live_ storage (invariant 8). On success the connection
becomes active and a connection.credential.store audit event is recorded.
If the workspace snapshot is not published, the endpoint answers with the
projection and stores nothing; retry after publication settles.
Complete a federated connection
POST /api/v1/workspaces/{workspace_id}/connections/{connection_id}/federated-complete
Consumes one issued cxs_ handoff and would activate the federated connection.
string
required
Must match
^cxs_[a-f0-9]{32}$.string
required
The broker’s own connection id, 1–256 bytes. Rejected with
400 invalid upstream connection id when it is padded, over 256 bytes, contains control
characters, or contains : or // — it is vendor-supplied and lands verbatim
in an audit detail.string
required
The broker’s MCP endpoint for this connection. Validated against the reviewed
origin allowlist frozen onto the session row at handoff time, then probed.
Anything refused is
400 mcp target is not allowed.