Authentication
Every/api/v1 route requires a dashboard session token — see
/api-reference/overview for how it is verified:
/api/v1 carry Cache-Control: no-store. A caller that is not
a member of the workspace gets 404, never 403 — an “exists but forbidden”
answer 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 non-read action.
GET /oauth/callback is the exception: it is unauthenticated because the vendor
redirects a browser to it. It is bound instead to a single-use, hashed OAuth
state row and to the operator who initiated the connection.
Start an OAuth connection
POST /api/v1/workspaces/{workspace_id}/connections
Requires the connection.manage action (admin or owner). Creates a pending
connection for a native OAuth catalog entry and, once the workspace snapshot is
published, returns an authorize URL for the operator’s browser.
string
required
Workspace the connection belongs to.
string
required
Catalog slug, for example
github.string
required
Catalog lane. Use
native for the gateway-owned OAuth path. nango and
composio take the federated path instead — see
/api-reference/catalog.string
Your own OAuth client id. No pooled Obol OAuth app exists: a connection uses
the customer’s own client (invariant 8). Falls back to the deployment’s
configured client id; when neither is present the request is refused with
400 oauth client id is required.string
Your OAuth client secret. Sent to the gateway to be sealed, then stored only
as ciphertext on the connection’s auth profile. Never echoed back.
Response
Every connection mutation answers with the same envelope.integer
Always
1.object
The
operator_connection projection: connection_id, workspace_id,
connector_slug, connector_display_name, source, tier, status,
custody, evidence_ceiling, last4, expires_at, last_refreshed_at,
refreshable, credential_revision, pending_catalog_session, and
publication. No broker_ref, target, or auth_profile ever leaves here.object
What the caller must do next. See The next object.
The next object
string
required
One of
none, redirect, catalog_redirect, or restricted_key.
The authorize URL is built from the connection’s stored OAuth profile plus a
fresh PKCE challenge:
client_id, redirect_uri, scope, state,
code_challenge, code_challenge_method=S256, and response_type=code. The
state value is returned only inside next.url and is never a separate field
in the response body. Its row stores only the SHA-256 hash, and it expires ten
minutes after issue.
Complete the vendor redirect
GET /oauth/callback
The redirect target the vendor sends the operator’s browser back to. It is
mounted at the root of the control app, not under /api/v1, and takes no
Authorization header.
string
Authorization code from the vendor. Absent on a denied or failed
authorization.
string
required
The opaque state issued in the authorize URL. Single use.
string
Vendor error code. When present, the code is ignored.
Behavior
The state row is consumed first and committed immediately, so a replayed callback finds nothing to consume. The callback then re-derives its authority from the row: the operator who started the connection must still exist, still be a member, and still holdconnection.manage, and the connection must still be
ready for OAuth. The exchange itself is a gateway call; control receives a sealed
envelope and writes it under a fresh workspace lock, records a
connection.credential.store audit event, and stages a snapshot publication.
Only two outcomes exist:
400 invalid or expired oauth statewhenstateis missing, malformed, unknown, expired, or already consumed. This one message covers all of them deliberately.302to the dashboard for everything else.
{OBOL_WEB_URL}/dashboard/connectors with two query
parameters:
string
connected when the credential is stored and the workspace snapshot
published, pending when it is stored but publication has not settled, and
error for a vendor error, a missing or malformed code, a failed exchange, or
an authorization that no longer holds.error is intentionally undifferentiated — the callback is a browser
destination, not a diagnostic surface. Use
the audit log to see whether a
connection.credential.store event was recorded.
Re-authorize or rotate
POST /api/v1/workspaces/{workspace_id}/connections/{connection_id}/reconnect
Requires connection.manage, and the connection must be pending. With an
empty body on a native OAuth connection, this bumps credential_revision,
deletes any outstanding OAuth state rows for the connection, and issues a fresh
redirect. With client_secret, it seals a replacement OAuth client secret
first, then issues the redirect. Full parameters, including the federated and
API-key modes, are on
/api-reference/catalog.
Reconnect
Revoke access
There is no OAuth revoke endpoint. Two connection endpoints cover the cases:POST /api/v1/workspaces/{workspace_id}/connections/{connection_id}/disablestops the connection serving traffic and leaves the credential sealed, so reconnecting restores it.POST /api/v1/workspaces/{workspace_id}/connections/{connection_id}/purge-credentialdestroys the sealed access and refresh envelopes, clearslast4andexpires_at, bumpscredential_revision, and leaves the row as the audit record of a credential the workspace once held. Irreversible.
Token refresh
Refresh has no HTTP surface. A scheduled control worker (refresh_expiring_credentials) claims connections whose credential is nearing
expiry under a row lock, calls the gateway’s
/internal/v1/credentials/refresh, writes the returned envelope, and republishes
the workspace snapshot. connection.refreshable in the projection is true
exactly when the connection’s auth profile type is oauth2.
See /security/oauth for the credential lifecycle and
/security/credential-brokering for how the
sealed envelope reaches an outbound call.