The authorization endpoint.
Unauthenticated at this step. Validates the request and redirects to
the SPA consent screen at /authorize carrying the parameters; it
does not render HTML, because this application serves one index.html
and a server-rendered consent page would be the only thing that could
drift from the app's own styling.
Failures concerning the redirect target itself are reported directly
rather than bounced off it — an unvalidated redirect_uri is an open
redirect and must not be used to deliver its own error. redirect_uri
is matched exactly against one the client registered; a prefix match
here would hand somebody else an authorization code.
PKCE is required and code_challenge_method must be S256.
Parameters
redirect_uri query required
Must match one the client registered exactly.
response_type query
code. Required by /oauth/authorize.
scope query
Space-separated grants. Unrecognised ones are dropped; none means the
minimal default.
code_challenge query
PKCE. Required by /oauth/authorize.
code_challenge_method query
S256, the only method accepted.
resource query
RFC 8707 — which resource the credential is for. Validated as naming
this instance, not merely recorded.
Responses
303 To the consent screen, or back to the client with an error.
400 Unknown client, or a redirect_uri that does not match its registration.
application/json
object
An OAuth error, as RFC 6749 shapes it.
error string required
invalid_request, invalid_client, invalid_grant, invalid_target,
invalid_redirect_uri, unsupported_grant_type, server_error.
error_description string required
Exchange a code, or refresh.
Unauthenticated (public clients, token_endpoint_auth_method: none);
form-encoded, as the RFC requires. Grants: authorization_code and
refresh_token.
The code is single use, taken with DELETE … RETURNING so two
simultaneous redemptions cannot both succeed. It is bound to the client,
the redirect, the PKCE challenge and the resource — every one of those
is re-checked here.
A refresh rotates both secrets, so a leaked access token stops
working as soon as the legitimate client refreshes. Access credentials
live 24 hours.
Request body required
application/x-www-form-urlencoded
object
Form-encoded, as the RFC requires.
grant_type string required
authorization_code or refresh_token.
redirect_uri string | null
code_verifier string | null
43–128 characters, per RFC 7636.
refresh_token string | null
Responses
200 A credential. Never cached.
application/json
object
An access credential. Served no-store.
access_token string required
token_type string required
expires_in integer · int64 required
refresh_token string | null
Rotates on every refresh; the old pair stops working.
scope string | null
What was actually granted, which can differ from what was asked for.
Absent on a refresh.
400 invalid_grant, invalid_request, invalid_target or unsupported_grant_type.
application/json
object
An OAuth error, as RFC 6749 shapes it.
error string required
invalid_request, invalid_client, invalid_grant, invalid_target,
invalid_redirect_uri, unsupported_grant_type, server_error.
error_description string required
RFC 7591 dynamic client registration.
Unauthenticated by design: this hands out an identifier and nothing
else. Every client is public, there is no secret, and possession of a
client_id gets you as far as an authorization request that a logged-in
human then has to approve by hand.
redirect_uris must be https, a loopback http address, or a private-use
scheme. Plain http anywhere else would put an authorization code on the
network in the clear.
Request body required
application/json
object
RFC 7591 registration. Open by design.
client_name string | null
Up to 80 characters. Shown on the consent screen as unverified.
redirect_uris array
At least one. https, loopback http, or a private-use scheme; no
fragment.
Responses
application/json
object
RFC 7591 registration response. The client_id is not a credential; it
gets a client as far as a request a signed-in person has to approve.
client_id string required
client_name string required
redirect_uris array required
token_endpoint_auth_method string required
grant_types array required
response_types array required
400 A redirect_uri was missing or not acceptable.
application/json
object
An OAuth error, as RFC 6749 shapes it.
error string required
invalid_request, invalid_client, invalid_grant, invalid_target,
invalid_redirect_uri, unsupported_grant_type, server_error.
error_description string required
POST /api/v1/oauth/approve # Session
The user said yes.
Session-only, and that is the load-bearing part of the whole flow:
minting a credential requires a logged-in human on this instance.
Everything before this point is a request; this is the consent.
redirect_uri is re-checked here rather than trusted from the browser —
this handler is reachable directly. The resource must name this
server, or RFC 8707's audience binding would record an audience and
enforce nothing. The response carries iss (RFC 9207) in the redirect.
Request body required
application/json
object
The authorization request being approved, and what the person ticked.
client_id string required
redirect_uri string required
code_challenge string required
scope string | null
What the person granted — which may be more or less than the client
asked for.
Responses
200 Where to send the browser next.
application/json
object
redirect_to string · uri required
The client's redirect_uri with code, iss and state added.
400 invalid_client, invalid_request or invalid_target.
application/json
object
An OAuth error, as RFC 6749 shapes it.
error string required
invalid_request, invalid_client, invalid_grant, invalid_target,
invalid_redirect_uri, unsupported_grant_type, server_error.
error_description string required
401 No valid session cookie or token. Also returned when a token is
presented to a session-only endpoint — the endpoint does not accept
tokens at all, so the scope is irrelevant.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
{
"code": 401,
"error": "Authentication required. Log in to access this endpoint."
}