Reference

OAuth and OIDC endpoints

Every endpoint of a tenant: discovery, authorize, token, userinfo, revoke, introspect, logout and device, with parameters and responses.

View .md llms.txt

Every endpoint is on the tenant's issuer: https://<tenant>.9in.io, https://<tenant>-sandbox.9in.io or your custom domain. <issuer> below stands for it.

MethodPathWhat
GET/.well-known/openid-configurationDiscovery document
GET/.well-known/jwks.jsonPublic signing keys
GET, POST/oauth/authorizeStart a sign-in
POST/oauth/tokenExchange a code, refresh, client credentials, device code, password
POST/oauth/device/authorizeStart a device sign-in (RFC 8628)
GET, POST/oauth/userinfoThe user's claims, for an access token
POST/oauth/revokeRevoke a refresh or access token (RFC 7009)
POST/oauth/introspectIs a token live? (RFC 7662)
GET, POST/oauth/logoutSign out (RP-initiated logout)
/api/v1/…The Management API

The hosted pages (/login, /register, /password/recovery, /account, /device, …) are for people, not for API calls; link to /account from your app if you like.

#Discovery

GET <issuer>/.well-known/openid-configuration returns the endpoints above and what's supported:

FieldValue
response_types_supportedcode, id_token, id_token token
response_modes_supportedquery, fragment, form_post
grant_types_supportedauthorization_code, refresh_token, client_credentials, urn:ietf:params:oauth:grant-type:device_code, password, implicit (the last two only for applications that enable them)
code_challenge_methods_supportedS256
token_endpoint_auth_methods_supportedclient_secret_basic, client_secret_post, none (public clients)
id_token_signing_alg_values_supportedThe algorithms of the tenant's keys
prompt_values_supportednone, login, consent, select_account, create
ui_locales_supportedThe tenant's languages
authorization_response_iss_parameter_supportedtrue
scopes_supportedThe standard scopes, plus every API's scopes

#GET /oauth/authorize

Redirect the browser here to sign a user in. POST with the same parameters as a form also works.

ParameterRequired
response_typeYescode. (id_token and id_token token only for applications with the implicit flow turned on.)
client_idYes
redirect_uriYesMust exactly match one of the application's callback URLs
scopeSpace-separated. Include openid for an ID token, offline_access for a refresh token. Default openid.
stateRecommendedReturned unchanged
code_challenge, code_challenge_methodPublic clientsPKCE: S256 only
nonceCopied into the ID token
audience (or resource)An API identifier: the access token is issued for that API
promptlogin (sign in again), none (no UI: fail with login_required or consent_required), consent, select_account, create (open the sign-up form)
max_ageSeconds: ask the user to sign in again if their last sign-in is older
login_hintPre-fills the identifier
ui_localesPreferred languages, e.g. de en
response_modequery (default for code), fragment, form_post

request and request_uri (signed request objects) are not supported.

Success: a redirect to redirect_uri?code=…&state=…&iss=<issuer>. Check state, and iss if your library supports it.

Errors: with a valid client and redirect URI, a redirect to redirect_uri?error=…&error_description=…&state=…: invalid_request, unauthorized_client, unsupported_response_type, invalid_scope, invalid_target (unknown audience), access_denied (the user declined), login_required, consent_required, request_not_supported. An unknown client or redirect URI shows an error page instead of redirecting.

#POST /oauth/token

Form-encoded (application/x-www-form-urlencoded). Confidential applications authenticate with HTTP Basic (client_id:client_secret, URL-encoded) or client_id and client_secret in the form; public applications send client_id. Browsers may call it from an allowed web origin (CORS).

grant_typeParameters
authorization_codecode, redirect_uri, code_verifier (PKCE)
refresh_tokenrefresh_token, optional narrower scope
client_credentialsOptional audience and scope (machine to machine)
urn:ietf:params:oauth:grant-type:device_codedevice_code (devices)
passwordusername, password, scope, optional audience (first-party applications that enable it; refused for users with two-factor)
json
{
  "access_token": "eyJ…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile email offline_access",
  "id_token": "eyJ…",
  "refresh_token": "…"
}

id_token comes with the openid scope; refresh_token with offline_access (when the application and the API allow refresh tokens). Refresh tokens rotate: each refresh returns a new one, and using an old one again revokes the whole family.

Errors (JSON { "error", "error_description" }): invalid_request, invalid_client (401), invalid_grant (bad or used code, wrong verifier, expired or revoked refresh token, wrong password), unauthorized_client (grant not allowed for this application), unsupported_grant_type, invalid_scope, invalid_target, access_denied, and for device codes authorization_pending, slow_down, expired_token.

#POST /oauth/device/authorize

client_id, scope, optional audience. Returns device_code, user_code (XXXX-XXXX), verification_uri (<issuer>/device), verification_uri_complete, expires_in (600) and interval (5). See TVs, CLIs and devices.

#GET /oauth/userinfo

Authorization: Bearer <access token> (a token with the openid scope). Returns sub plus the claims the token's scopes allow (scopes and claims) and custom fields marked as claims. Revoked tokens get 401.

#POST /oauth/revoke

token, optional token_type_hint (refresh_token or access_token), with client authentication (or client_id for public clients). Revoking a refresh token revokes its family; revoking a JWT access token puts its jti on a deny-list checked by introspection and hybrid verification. Always answers 200 for an unknown token, as RFC 7009 requires.

#POST /oauth/introspect

token, authenticated as a confidential application of the tenant. Live state: revoked tokens, ended sessions and blocked users are { "active": false }.

json
{
  "active": true,
  "token_type": "access_token",
  "scope": "orders:read",
  "client_id": "app_7f3k2m9q",
  "sub": "usr_8k2…",
  "aud": "https://api.acme.com",
  "iss": "https://acme.9in.io",
  "exp": 1794203600,
  "iat": 1794200000,
  "jti": "…",
  "sid": "…",
  "auth_time": 1794199000
}

#GET /oauth/logout

Ends the 9in session, then redirects to post_logout_redirect_uri (with state) or shows a "You're signed out" page.

Parameter
id_token_hintAn ID token the tenant issued (expired is fine): identifies the application
client_idIdentifies the application when there's no id_token_hint
post_logout_redirect_uriOne of the application's allowed logout URLs
stateAdded to the redirect

#GET /.well-known/jwks.json

The public keys that verify tokens: every active and retired asymmetric key (RS, PS, ES, EdDSA). HMAC keys are never published. Cache it; fetch it again when a token names a kid you don't know.