OAuth
Connect apps and agents to Talonic with OAuth 2.1 and PKCE: discovery, dynamic client registration, scopes and defaults, the tier claim, and apps:decide.
Besides API keys, Talonic runs an OAuth 2.1 authorization server so an application or agent can act for a person in a workspace without ever handling an API key. It supports the authorization-code grant with PKCE (S256 only) and refresh tokens, for public clients (token_endpoint_auth_methods_supported: ["none"]). A client discovers the endpoints, registers itself dynamically, sends the person to the consent page, and exchanges the code for an access token it presents as Authorization: Bearer <token> on the /v1 API, exactly where an API key would go.
Discovery and registration
GET /.well-known/oauth-authorization-server returns the server metadata (RFC 8414): issuer, authorization_endpoint (/oauth/authorize), token_endpoint (/oauth/token), userinfo_endpoint, registration_endpoint (/oauth/register), revocation_endpoint (/oauth/revoke), scopes_supported, response_types_supported: ["code"], grant_types_supported: ["authorization_code", "refresh_token"], and code_challenge_methods_supported: ["S256"]. Clients connecting through Talonic's hosted MCP server also find protected-resource metadata at /.well-known/oauth-protected-resource on the MCP host, which points them at this authorization server.
Dynamic client registration (POST /oauth/register, RFC 7591) needs no authentication and is rate-limited per IP. Send client_name (required) and redirect_uris (1 to 10; https://, or http:// only for localhost), plus optional client_uri and grant_types. The response returns your client_id with token_endpoint_auth_method: "none". Then send the person to /oauth/authorize with response_type=code, client_id, redirect_uri, code_challenge, code_challenge_method=S256, scope, and state, and exchange the returned code at /oauth/token with your code_verifier. Authorization codes are single-use and expire after 10 minutes; access tokens last one hour; refresh tokens last 30 days and rotate on every use.
Discover, register, and exchange a code
curl -s https://api.talonic.com/.well-known/oauth-authorization-server
curl -s -X POST https://api.talonic.com/oauth/register \
-H "Content-Type: application/json" \
-d '{ "client_name": "Ops Assistant", "redirect_uris": ["https://ops.example.com/callback"] }'
curl -s -X POST https://api.talonic.com/oauth/token \
-d grant_type=authorization_code -d client_id=CLIENT_ID -d code=CODE \
-d redirect_uri=https://ops.example.com/callback -d code_verifier=VERIFIERToken response
{
"access_token": "eyJhbGciOi…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "…",
"scope": "documents:read apps:read"
}Scopes and defaults
Requestable scopes
If the authorization request omits scope, the token receives the five default scopes (extract:write, documents:read, schemas:read, sources:read, webhooks:manage). The Apps scopes, records:write, apps:decide, and identity:read are never granted implicitly: a client gets them only by naming them. An unknown scope is a 400. An installed App can pre-consent apps:read, apps:operate, records:write, and identity:read for the workspace, so a request naming only those skips the consent page; every other scope needs the person to click Authorize.
Role, tier, and apps:decide
Every access token carries the person's workspace role. When the scopes reach the Apps surface, the token also carries a `tier` claim: read, run, operate, or configure. Tiers are cumulative, so a tier admits everything below it, and the token's tier is the lower of what its scopes reach and what the person's role reaches (Viewer → read, Member → run, Senior Member → operate, Owner → configure). A token can never outrank the person it was issued to. A route the token's tier does not reach is refused with 403 and reason: insufficient_tier; a missing scope is refused with reason: insufficient_scope and required_scopes.
`apps:decide` lets an OAuth session claim and decide App decision tasks as the person. It is never pre-consented: the person consents to it in person, every time. Holding the scope is not enough on its own. Each decide request re-reads the person's live membership, which must be active and at Senior Member or above; a lower role is refused with insufficient_tier, and no active membership with decide_grant_required. Membership is also checked when a token is refreshed: a person who has left the workspace cannot refresh, the presented refresh token is revoked, and the request fails with 401.
insufficient_tier, decide_grant_required, and a 401 on refresh as normal outcomes and send the person back through authorization.