Integrate / OAuth

OAuth apps (PKCE)

Act on a member's behalf with per-scope consent that the member or the organization can revoke at any time — never beyond the member's own permissions.

Register an app

POST /api/v1/oauth-apps (oauth_app:write) registers a public client — CLIs, desktop and mobile apps, PKCE and no secret — or a confidential one, whose client_secret (ycs_…) is returned once and rotated with POST /api/v1/oauth-apps/{app_id}/rotate-secret. Redirect URIs are absolute, https or loopback http, and matched exactly; the app's scopes bound what any consent may ask for.

# POST /api/v1/oauth-apps                                     (oauth_app:write)
{ "name": "Acme CLI", "app_type": "public",
  "redirect_uris": ["http://127.0.0.1:8765/callback"],
  "scopes": ["openid", "usage:read"], "homepage": "https://acme.dev" }

# 201 — client_secret: null for public apps, shown once for confidential
{ "app": { "id": "oapp_01J…", "client_id": "ycl_9f2k…a71", "state": "active", "has_secret": false, … },
  "client_secret": null }

Authorization code with PKCE

Consent is per scope and per user, given on the console's consent screen and revocable at any time — by the user (GET /api/v1/oauth/grants, POST …/grants/{grant_id}/revoke) or by the organization (/api/v1/oauth-apps/grants). Revoking a grant or an app revokes every token issued under it in the same transaction. PKCE with S256 is required for every client, public or confidential.

# 1. send the user to the consent screen with a PKCE challenge
GET https://yelno.ai/auth/oauth/authorize
  ?client_id=ycl_9f2k…a71
  &response_type=code
  &redirect_uri=https://app.example.com/callback
  &scope=openid%20usage:read
  &code_challenge=<base64url(sha256(verifier))>&code_challenge_method=S256
  &state=…
# allow → https://app.example.com/callback?code=…&state=…
# deny  → https://app.example.com/callback?error=access_denied&state=…

# 2. exchange the code (form-encoded or JSON;
#    confidential clients add client_secret in the body or HTTP Basic)
POST /api/v1/oauth/token
  grant_type=authorization_code&code=…&redirect_uri=https://app.example.com/callback
  &client_id=ycl_9f2k…a71&code_verifier=…

{ "access_token": "<JWT>", "token_type": "Bearer", "expires_in": 3600,
  "refresh_token": "yrt_…", "scope": "openid usage:read", "organization_id": "org_01J8…" }

# 3. call the management API as the user
Authorization: Bearer <access_token>      # 1 h; grant_type=refresh_token renews (30 d, rotating)
GET /api/v1/oauth/userinfo                # openid → sub, organization_id, azp (+ email scope)
GET /api/v1/usage                         # usage:read

Scopes

A token can never exceed the granting member's own permissions: each scope maps onto management permissions that are AND-ed with the member's live role, so a scope only ever narrows. No scope covers API keys' secret material, provider credentials or content — and none covers inference: user tokens drive the management API, not /v1/chat/completions. GET /api/v1/oauth/scopes serves this table for the consent screen.

ScopeDescription at consentPermissions
openidSign you in and identify your account— (required for /oauth/userinfo)
emailRead your email address— (adds email to userinfo)
usage:readRead usage, cost and request summariesusage:read · org:read
requests:readRead individual request recordsusage:read
wallet:readRead wallet balances and credit grantswallet:read
keys:readList API keys (never their secret material)key:read
catalog:readRead the model catalog as your organization sees itcatalog:read
projects:writeCreate and update projectsproject:read · project:write

Tokens and revocation

Access tokens are JWTs that live one hour and carry scp, azp (the client id) and the member's live role. Refresh tokens live 30 days and rotate on every use: presenting an already-rotated refresh token revokes every token of the grant, and presenting an authorization code twice revokes the tokens it produced. POST /api/v1/oauth/revoke (RFC 7009) revokes a token and always answers 200. A revoked token gets 401 token_revoked; a call outside the granted scopes gets 403 insufficient_scope. Token-endpoint errors use the house envelope with the RFC 6749 code in error.code — invalid_request, invalid_client (401), invalid_grant, unsupported_grant_type, invalid_scope.