Endpoint reference
Base URL: https://api.nanako.org. The same paths are served from
https://www.nanako.org.
GET /oauth/authorize
Starts the flow. This is a browser navigation, not a request your server makes.
Authentication: none.
| Parameter | In | Required | Default | Notes |
|---|---|---|---|---|
client_id | query | Yes | — | |
redirect_uri | query | Yes | — | Byte-identical to a registered URI |
response_type | query | No | code | code is the only accepted value |
scope | query | No | openid profile | Space-separated; send it explicitly |
state | query | No | empty | Echoed to the callback unescaped — keep it URL-safe |
code_challenge | query | No | empty | PKCE |
code_challenge_method | query | No | plain | Only the exact string S256 selects SHA-256 |
prompt | query | No | empty | none requests a no-UI attempt |
Success: 302 to Nanako's sign-in and consent page. No authorization code
exists yet at this point — the code reaches your callback after the user
approves.
Errors: returned as JSON at this endpoint, not redirected to your callback. See Errors.
prompt=none
Requests that Nanako complete the flow without showing any interface. Nanako redirects to your callback with an error instead of rendering a page:
| Situation | Callback receives |
|---|---|
| Not signed in | ?error=login_required |
| Signed in, no matching grant | ?error=consent_required |
| The grant check failed | ?error=interaction_required |
| Signed in with a matching grant | ?code=… — no consent screen |
state is re-appended in every case. On any of the three errors, retry without
prompt=none to fall back to the visible flow.
This is a top-level navigation, not an iframe: Nanako sends
X-Frame-Options: DENY and a frame-blocking CSP on every response, so the
hidden-iframe pattern used with some providers will not work.
POST /oauth/token
Exchanges an authorization code for tokens, or refreshes an existing pair.
Authentication: client_secret_post, or PKCE for the authorization code
grant. HTTP Basic is not supported.
Content-Type: application/x-www-form-urlencoded. A JSON body is not read.
grant_type=authorization_code
| Parameter | Required | Notes |
|---|---|---|
grant_type | Yes | authorization_code |
code | Yes | From the callback, within 10 minutes, unused |
client_id | Yes | |
redirect_uri | Yes | Byte-identical to the one used at /oauth/authorize |
client_secret | Conditional | Required unless the code was issued with a code_challenge |
code_verifier | Conditional | Required when the code was issued with a code_challenge |
Which credential is required is decided by how the code was issued, not by what you send. A code issued without PKCE requires the secret; a code issued with PKCE requires the verifier.
grant_type=refresh_token
| Parameter | Required | Notes |
|---|---|---|
grant_type | Yes | refresh_token |
refresh_token | Yes | The most recently issued one |
client_id | Yes | |
client_secret | Yes | Always — there is no PKCE branch on this grant |
Response
200 for both grants:
{
"access_token": "aB3dE5fG7hJ9kL1mN3pQ5rS7tU9vW1xY3zA5bC7dE9=",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "zY9xW7vU5tS3rQ1pN9mL7kJ5hG3fE1dC9bA7zY5xW3=",
"scope": "profile email"
}
| Field | Type | Notes |
|---|---|---|
access_token | string | Opaque, 44 characters. Valid 2 hours. |
token_type | string | Always Bearer |
expires_in | number | Always 7200 |
refresh_token | string | New on every call. The previous one is now revoked. |
scope | string | The granted scope |
There is no id_token. See What is not supported.
GET /oauth/userinfo
Returns the signed-in user's profile.
Authentication: Authorization: Bearer <access_token>. The prefix is
case-sensitive.
Response: 200. sub is always present; every other claim depends on the
token's scope and on the user having set the value.
{
"sub": "0198c4a1-7f3e-7b21-9d04-1a2b3c4d5e6f",
"name": "hana",
"picture": "https://cdn.nanako.org/avatars/…",
"email": "hana@example.com",
"email_verified": true,
"phone_number": "+8613800138000",
"phone_number_verified": true
}
| Claim | Scope | Notes |
|---|---|---|
sub | always | Stable, permanent user identifier. Key your records on this. |
name | profile | Display name; the user can change it |
picture | profile | Avatar URL; empty string if unset |
email | email | Omitted entirely if the account has no email |
email_verified | email | |
phone_number | phone | Omitted entirely if the account has no phone |
phone_number_verified | phone |
sub has two formatsAccounts created recently have a UUID sub
(0198c4a1-7f3e-7b21-9d04-1a2b3c4d5e6f). Accounts that predate a schema
migration keep a numeric one (42). Both are stable and permanent for the
account they identify.
Treat sub as an opaque string. Do not parse it, and do not assume it is a
UUID.
POST /oauth/revoke
Revokes one token.
Authentication: client_secret_post. Client credentials are mandatory, so
a public (PKCE) client cannot call this endpoint.
Content-Type: application/x-www-form-urlencoded.
| Parameter | Required | Notes |
|---|---|---|
client_id | Yes | |
client_secret | Yes | |
token | Yes | The token to revoke |
token_type_hint | Conditional | Required to revoke a refresh token: refresh_token |
Response: 200 {}.
The success response does not confirm that anything was revoked. A token that
does not exist, belongs to another application, or was submitted without the
matching token_type_hint all produce the same 200 {}. This is deliberate for
the first two cases — it avoids disclosing which tokens exist — but it means
the hint really matters. Revoke access and refresh tokens in two separate calls,
each with its explicit hint.
GET /.well-known/openid-configuration
Returns provider metadata.
token_endpoint, userinfo_endpoint and revocation_endpoint are advertised
with an /api/v1 prefix that does not exist, and all three return 404.
issuer also differs from the host the document is served from, which some
strict OIDC libraries reject outright.
Do not point a client library at this URL. Configure the endpoints from this page.
For reference, the document advertises scopes_supported as openid,
profile, email, phone; response_types_supported as code;
grant_types_supported as authorization_code and refresh_token;
token_endpoint_auth_methods_supported as client_secret_post; and
code_challenge_methods_supported as plain and S256. Those five are
accurate.
It also advertises jwks_uri and id_token_signing_alg_values_supported: ["RS256"]. Ignore both — no id_token is issued, and the key served at that
URL belongs to an unrelated internal integration.