Sign-in (OAuth2 / OIDC)
Endpoint reference for Sign in with Ppoppo. For a step-by-step walkthrough see the OAuth2 PKCE guide; for your first integration see the Quickstart.
All sign-in endpoints are served by PAS:
| Environment | Base URL |
|---|---|
| Production | https://accounts.ppoppo.com |
| Sandbox | https://accounts.sandbox.ppoppo.com |
Ppoppo supports exactly one flow: Authorization Code with PKCE (S256). There is no implicit flow, no password grant, and no client_secret for login — see Trust boundary for why.
Authorization endpoint
GET /oauth/authorize| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your login client ID |
redirect_uri | Yes | A registered callback URL (exact match) |
response_type | Yes | Always code |
scope | Yes | Space-separated scopes (see Scopes) |
state | Recommended | Opaque value echoed back for session correlation (PKCE itself provides CSRF protection) |
code_challenge | Yes | PKCE challenge — base64url-encoded SHA-256 of your code_verifier |
code_challenge_method | Yes | Always S256 |
On success Ppoppo redirects to your redirect_uri with code (valid 5 minutes, single use) and your state. On failure it appends error + error_description instead — see Errors.
Token endpoint
POST /oauth/token
Content-Type: application/x-www-form-urlencodedExchange an authorization code
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://yourapp.com/auth/callback
&client_id=yourapp_login_client
&code_verifier=ORIGINAL_CODE_VERIFIERThe code_verifier authenticates the client — there is no client_secret in this request.
{
"access_token": "eyJ...",
"refresh_token": "rt_01HQ...",
"id_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email"
}| Token | Format | Lifetime |
|---|---|---|
access_token | JWT (RFC 9068, EdDSA over Ed25519) | 1 hour. Treat as opaque — do not verify it yourself. |
refresh_token | opaque string | Valid while in use; expires after 180 days of inactivity. |
id_token | JWT (OIDC Core 1.0, EdDSA) | Returned only when the openid scope is granted. |
Refresh an access token
grant_type=refresh_token
&refresh_token=rt_01HQ...
&client_id=yourapp_login_clientThe response carries a new access_token; the refresh_token field is null because the same refresh token stays valid (no rotation). Regular use keeps it alive indefinitely.
UserInfo endpoint
GET /oauth/userinfo
Authorization: Bearer <access_token>| Field | Type | Description |
|---|---|---|
sub | string | The user's ppnum_id (ULID) — your stable key for this user. |
ppnum | string | The user's Ppoppo number (≥11 digits). |
email | string | Present only when the email scope was granted. |
email_verified | boolean | Present with email; always true (Ppoppo verifies every email). |
created_at | string | Account creation timestamp (RFC 3339). |
account_type | string | Present only for non-human accounts (e.g. ai_agent); omitted for regular users. |
Ppoppo does not return a username or display name — manage display names in your own service. Store sub (ppnum_id) and ppnum; never cache email, which is mutable and owned by PAS. Fetch it from UserInfo when you need it. See The ppnum model.
Scopes
| Scope | Effect |
|---|---|
openid | Enables OIDC; an id_token is returned alongside the access token. |
profile | Returns ppnum from UserInfo. |
email | Returns email / email_verified from UserInfo. |
messaging | Authorizes the Messaging External API. |
poll | Authorizes the Poll External API. |
The External API is called with a separate client_credentials token, not the login token — see the Messaging reference.
Errors
OAuth2 error codes are listed in the shared Errors reference.