The catalog is a frozen index shipped with the image (ADR-0030). These endpoints read it, attach the workspace’s own connections to it, and move a connection between 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 require Authorization: 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 same schema_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.
Also returned: 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.
Answers 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.
The lane and the catalog entry’s auth modes decide what comes back in 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.
A catalog connect session lives ten minutes and can be consumed once.

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_.
Sending both is 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.
The connection’s auth profile must be 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.
Federated completion is gated. complete_federated_session refuses every well-formed request with 409 broker_verification_unavailable: a HEAD probe proves a reviewed, reachable origin but not who owns the connected account, and no authenticated broker-account verifier exists yet. An operator-supplied identifier cannot be authority for a credentialed route (ADR-0030, invariants 8 and 9). The handoff and the pending connection are left untouched, so nothing is consumed.

Refusals

One fixed detail per closed outcome, so a caller cannot tell “no such session” from “expired”, “consumed”, “wrong operator”, or “wrong provider”:
See /connectors/federated for what a federated route can and cannot prove, and /routing/catalog for how a connection reaches a snapshot.