Skip to content

Connected accounts

Connected accounts solve a problem that turns up as soon as an application or an agent has to act against a third-party API on a user’s behalf: somebody has to run the OAuth dance, store the refresh token, refresh it before it expires, and keep it out of everything that does not need it.

Here, the platform does all four. Your org registers its OAuth app, users grant access once through a brokered flow, tokens are stored envelope-encrypted, and your services ask the management API for a live access token when they need one. The refresh token never leaves the vault.

Requires connect:write. Named kinds come with their endpoints already set:

POST /v1/organizations/org_01J…/connect/providers
Authorization: Bearer sk_live_…
{
"name": "GitHub (issues)",
"kind": "github",
"client_id": "Iv1.…",
"client_secret": "",
"scopes": ["repo", "read:org"]
}
{
"provider_id": "cp_…",
"name": "GitHub (issues)",
"kind": "github",
"client_id": "Iv1.…",
"authorization_endpoint": "https://github.com/login/oauth/authorize",
"scopes": ["repo", "read:org"],
"created_at": 1750000000000
}
kindEndpoints
githubPreset
googlePreset
slackPreset
customYou supply authorization_endpoint and token_endpoint

The client_secret is stored encrypted and never returned. Both endpoints are checked against the egress policy before the provider is created.

These are your org’s OAuth apps — you register them with the provider, and the redirect URI you configure there points back at the tenant host.

A signed-in user sees what they have linked and what they could link:

GET /connect
Cookie: <tenant session>
{
"accounts": [
{ "account_id": "ca_…", "provider_id": "cp_…", "scopes": ["repo"], "status": "active", "expires_at": 1750003600000 }
],
"available": [
{ "provider_id": "cp_…", "name": "Slack", "kind": "slack", "scopes": ["chat:write"] }
]
}

Starting a link at /connect/:providerId/start redirects the user to the provider with PKCE and a single-use state; the provider returns to /connect/:providerId/callback, where the code is exchanged server-side and the tokens are encrypted and stored.

What a user is shown about a provider is deliberately narrower than what an operator sees: the client id and authorization endpoint are integration detail, while the scopes — what they are being asked to grant — are not. Users can disconnect their own accounts at /connect/:accountId/revoke.

These are hosted, same-origin endpoints backed by the session cookie; they are not callable cross-origin. See authentication.

This is the point of the vault. Requires connect:tokens:

POST /v1/organizations/org_01J…/connect/accounts/ca_…/token
Authorization: Bearer sk_live_…
{
"access_token": "gho_…",
"expires_at": 1750003600000,
"scopes": ["repo", "read:org"]
}

If the stored token is within a minute of expiry, it is refreshed against the provider during this call and the fresh one is returned — your service never has to hold refresh logic, or a refresh token. If the token has expired and no refresh token is held, the call returns 502 token_unavailable rather than handing back something dead.

Every retrieval writes an account_token_retrieved audit event naming the API key that made it. Token egress is the most sensitive read in the system and is treated as such — alert on it.

GET /v1/organizations/:orgId/connect/accounts?user_id=usr_… # connect:read
POST /v1/organizations/:orgId/connect/accounts/:accountId/revoke # connect:write
GET /v1/organizations/:orgId/connect/providers
DELETE /v1/organizations/:orgId/connect/providers/:providerId

Revoking marks the account inactive and stops token retrieval. The user can link the provider again afterwards — a revoked account still occupies its provider slot, which is why it reappears under available once revoked rather than being silently duplicated.

Both involve a third-party identity provider, and they are not the same thing:

  • OIDC federation / SAML — how a user signs in to your application. The result is a session and an ID token.
  • Connected accounts — how your application acts against another API as that user. The result is a stored, refreshable third-party access token.

A customer may well use both, with the same upstream provider, for different purposes.