Authorized applications
GET /v1/oauth/grants and DELETE /v1/oauth/grants/:clientId — the only entry point to authorization relationships.
Authorization relationships are created by oauth when it issues tokens, and only oauth knows how many sessions an application currently holds. The auth context only knows "which devices the user signed in on"; its session records contain no client authorization data, so this data is not duplicated on the auth side.
Endpoints
| Method | Path | Purpose | Requirements |
|---|---|---|---|
GET | /v1/oauth/grants | List authorized applications | aud=oauth, scope oauth:grants:read |
DELETE | /v1/oauth/grants/:clientId | Revoke an application's authorization | aud=oauth, scope oauth:grants:revoke, max_age=300 |
The caller is the management console: it uses RFC 8693 token exchange to turn an auth session token into a short-lived oauth-audience resource token (5 minutes, never persisted, audience trimmed to oauth / account).
GET /grants
Query
| Parameter | Type | Required | Notes |
|---|---|---|---|
limit | number | Optional | Default 20, max 100 |
cursor | string | Optional | Cursor from the previous page |
kind | first_party | third_party | Optional | Fetch only one section |
Response
{
"success": true,
"grants": [
{
"client_id": "cli_...",
"name": "Example App",
"client_type": "third_party",
"did": null,
"scopes": ["openid", "profile"],
"session_count": 2,
"authorized_at": "2026-10-02T12:00:00.000Z",
"last_issued_at": "2026-11-13T09:21:00.000Z"
}
],
"next_cursor": null
}| Field | Notes |
|---|---|
did | Device id for a first-party authorization; null for third parties |
user_agent | Raw UA of that device's most recent sign-in; absent for third parties |
session_count | Sessions this application currently holds |
scopes | Currently effective scope set |
Ordered by most recent issuance. First-party clients are aggregated per device, so the same application may appear on several rows — each with a different did, revocable individually.
Why fetch by kind
Each section paginates independently. If they were merged into one "most recently active" page and split client-side, a section would look empty while its entries actually sat on later pages.
DELETE /grants/:clientId
Parameters
| Parameter | Location | Required | Notes |
|---|---|---|---|
clientId | path | ✅ | Application to revoke |
did | query | Optional | Revoke only that device; the whole application when omitted |
{ "success": true, "message": "Authorization revoked", "revoked": 2 }| Semantics | Notes |
|---|---|
Without did | Third-party semantics: kill the application's access on every device |
With did | First-party semantics: kill the authorization on that one device |
did is a query parameter, not a path segment
It narrows the same relationship; it is not another resource. An extra path segment would make "application" and "device" look like two levels of entity.
Revocation is a one-shot destructive action, so it requires max_age=300; the read-only list does not. It does not accept RFC 9396 authorization details — that belongs to /authorize.
Related
- Getting an
oauth-audience token: Token exchange - User-facing FAQ: Revoking access
- First-party device sessions: Sessions