---
title: OAuth endpoints
path: reference/oauth-endpoints
status: published
---

# OAuth endpoints

ScaiKey exposes a full OIDC provider surface in two flavors: **platform** (for `GLOBAL`-scoped apps) and **tenant-scoped** (for apps that belong to one tenant).

## Discovery

Always start by fetching the discovery document — it's the source of truth for endpoint URLs and supported scopes.

| Flavor | URL |
|---|---|
| Platform | `$SCAIKEY/api/v1/platform/oauth/.well-known/openid-configuration` |
| Tenant | `$SCAIKEY/api/v1/auth/tenants/{slug}/.well-known/openid-configuration` |

The discovery doc lists every other endpoint. Read it once at startup with your OIDC library and you mostly don't need this page.

## Authorize endpoint

Starts the `authorization_code` flow. User-facing — your app redirects the browser here.

```
GET /api/v1/platform/oauth/authorize
GET /api/v1/auth/tenants/{slug}/oauth/authorize
```

Standard OIDC query parameters: `response_type=code`, `client_id`, `redirect_uri`, `scope`, `state`, `nonce`. PKCE: `code_challenge`, `code_challenge_method=S256`.

Optional: `prompt=login` (force re-auth), `prompt=none` (silent — fails if no active session), `login_hint` (prefill email), `idp_hint` (preselect a federated IdP).

The platform variant redirects unauthenticated users to a tenant-agnostic login page where they enter their email; ScaiKey then determines their tenant. The tenant variant goes straight to that tenant's login page.

## Token endpoint

The workhorse — exchange a grant for tokens.

```
POST /api/v1/platform/oauth/token
POST /api/v1/auth/tenants/{slug}/oauth/token
```

Content-Type: `application/x-www-form-urlencoded`. Client authentication via `Authorization: Basic` (recommended) or `client_id` + `client_secret` form fields.

### `grant_type=authorization_code`

| Field | Required | Notes |
|---|---|---|
| `code` | yes | The code from the authorize redirect |
| `redirect_uri` | yes | Must match what was sent to authorize |
| `code_verifier` | required for public clients | PKCE verifier |
| `client_secret` | required for confidential clients | |

### `grant_type=client_credentials`

| Field | Required | Notes |
|---|---|---|
| `client_secret` | yes | Confidential client only — `SPA`/`NATIVE` not allowed |
| `scope` | no | Space-separated scopes. If omitted, defaults to the app's full `allowed_scopes` |

### `grant_type=refresh_token`

| Field | Required | Notes |
|---|---|---|
| `refresh_token` | yes | The token to exchange |

Refresh tokens are one-time-use — the response includes a new refresh token; the previous one is invalidated.

### `grant_type=urn:ietf:params:oauth:grant-type:token-exchange`

| Field | Required | Notes |
|---|---|---|
| `subject_token` | yes | The token to exchange (must be ScaiKey-issued) |
| `subject_token_type` | yes | `urn:ietf:params:oauth:token-type:access_token` is the only supported value |
| `audience` | yes | The target application's `client_id` or name (case-insensitive lookup) |
| `requested_token_type` | no | Always returns `access_token` regardless |
| `scope` | no | Requested scopes; intersected with subject and target |
| `client_secret` | yes | Requesting app must be confidential |

The target application's `token_exchange_allowed` must be true. See [Concepts → OAuth and OIDC](/docs/scaikey/concepts/oauth-and-oidc#token-exchange) for the full rules.

### `grant_type=urn:ietf:params:oauth:grant-type:device_code`

| Field | Required | Notes |
|---|---|---|
| `device_code` | yes | From the device-authorize call |

Poll this endpoint at the interval in the device-authorize response (default 5 s). Responds with `authorization_pending` until the user approves, then issues tokens.

## UserInfo endpoint

```
GET /api/v1/platform/oauth/userinfo
GET /api/v1/auth/tenants/{slug}/oauth/userinfo
```

Bearer-authenticated. Returns OIDC identity claims for the user the token belongs to. `client_credentials` tokens (no user) get a 401 here.

## JWKS endpoint

```
GET /api/v1/platform/.well-known/jwks.json
GET /api/v1/auth/tenants/{slug}/.well-known/jwks.json
```

The platform and tenant flavors return the same keys (signing is shared). Use whichever your library is pointed at.

## Revoke endpoint (tenant-scoped only)

```
POST /api/v1/auth/tenants/{slug}/oauth/revoke
```

Revokes a refresh token. RFC 7009. Returns `200 OK` even if the token was unknown (anti-enumeration).

## Introspect endpoint (tenant-scoped only)

```
POST /api/v1/auth/tenants/{slug}/oauth/introspect
```

RFC 7662. Returns `{ "active": true, ...claims }` for a valid access token, `{ "active": false }` otherwise. Useful for opaque-token consumers (though all ScaiKey tokens are JWTs and can be self-verified).

## End_session endpoint (RP-initiated logout)

```
GET /api/v1/auth/tenants/{slug}/oauth/logout   ← tenant
GET /api/v1/platform/oauth/logout              ← platform (GLOBAL apps)   {#platform-end-session}
```

Query parameters: `post_logout_redirect_uri`, `id_token_hint`, `state`, `client_id`.

Terminates the SSO session (revokes the session cookie), then redirects to `post_logout_redirect_uri` if provided, otherwise to the default logout confirmation page.

For `GLOBAL` apps that don't have a tenant slug to put in the URL, use the platform variant — it identifies the session via the SSO cookie, not the URL path.
