Integration guide
Register an application and use OAuth 2.0 to request access to user data.
Register an application
In the developer workspace, enter an application name and exact callback URIs, then choose whether approval requires a verified email. Callbacks must use HTTPS; HTTP is allowed only on loopback addresses for local development. The client secret is shown once after creation. Save it securely if your server will use it.
Authorization code flow
CP OAuth supports the OAuth 2.0 authorization code flow with a client secret, PKCE, or both.
- Redirect the user to /oauth/authorize with your client ID, registered callback URI and requested scopes. Include state to verify the callback.
- The user reviews the requested access and chooses whether to approve it.
- On approval, your callback receives an authorization code. Validate the returned state before continuing.
- Exchange the single-use code for an access token and refresh token at POST /api/oauth/token.
- Call GET /api/oauth/userinfo with the Bearer access token to read the data covered by the granted scopes.
- Refresh at POST /api/oauth/token with grant_type=refresh_token. Omit client_secret only if the initial PKCE code exchange omitted it.
This is not a full OpenID Connect provider. The openid scope returns a user ID, not an ID token; no JWKS endpoint is published.
Server metadata: /.well-known/oauth-authorization-server
The previous metadata URL is also supported: /.well-known/openid-configuration
CP OAuth's own pages use HttpOnly session cookies and CSRF protection. Your integration uses grant credentials for token exchange and revocation, and a Bearer access token for userinfo. Do not use or forward the site's session cookie.
PKCE
Public applications can exchange authorization codes using PKCE without a client secret. Always send code_verifier when exchanging a PKCE code.
The verifier must be 43–128 characters: letters, digits, dot, underscore, tilde or hyphen. An S256 challenge is a 43-character base64url SHA-256 hash. The plain method is also supported and is the default when the method is omitted.
When the initial PKCE exchange omits the secret, refresh and revoke requests for that grant can also omit it. Grants created before the migration still require a secret until the user authorizes again.
// 32 random bytes give a 43-character base64url verifier.
const verifierBytes = crypto.getRandomValues(new Uint8Array(32))
const codeVerifier = btoa(String.fromCharCode(...verifierBytes))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
// Keep state and verifier securely for the same-browser callback; validate state there.
const authorizationUrl = new URL('https://www.cpoauth.com/oauth/authorize')
authorizationUrl.search = new URLSearchParams({
response_type: 'code',
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'https://yourapp.com/callback',
scope: 'openid profile',
state: crypto.randomUUID(),
code_challenge: codeChallenge,
code_challenge_method: 'S256'
}).toString()
// Exchange the returned code with code_verifier: codeVerifier.
// Omit client_secret entirely for a public grant; its refresh/revoke also need no secret.
// An empty or incorrect client_secret is never accepted, even with valid PKCE.Exchange a code without a secret
// Exchange a PKCE authorization code; do not send client_secret (not even an empty value).
const response = await fetch('https://www.cpoauth.com/api/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code: 'AUTHORIZATION_CODE',
redirect_uri: 'https://yourapp.com/callback',
client_id: 'YOUR_CLIENT_ID',
code_verifier: codeVerifier
})
})
const { access_token, refresh_token, expires_in, scope } = await response.json()
// This grant's refresh and revoke requests also omit client_secret.Server-side authentication
Keep client_secret on your server, not in browser code. Client secret authentication can be used with or without PKCE. If the authorization request used PKCE, include code_verifier in the code exchange.
If the initial code exchange includes a secret, refresh and revoke requests for that grant must also include the client secret. Missing, empty or incorrect secrets are rejected.
// Secret-authenticated applications: run this on your server, never expose the secret in a browser.
const response = await fetch('https://www.cpoauth.com/api/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code: 'AUTHORIZATION_CODE',
redirect_uri: 'https://yourapp.com/callback',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
code_verifier: 'YOUR_CODE_VERIFIER' // required if the authorization request used PKCE
// Only a secret-authenticated grant requested without PKCE can omit code_verifier.
})
})
const {
access_token, // JWT, expires in 1 hour
refresh_token, // opaque, expires in 30 days
token_type,
expires_in,
scope
} = await response.json()API endpoints
GET /oauth/authorize
Starts the consent flow. The callback URI must exactly match a registered URI: HTTPS, or HTTP on localhost, 127.0.0.1 or [::1]. A user can deny access without signing in; the callback receives access_denied and the original state.
GET https://www.cpoauth.com/oauth/authorize?
response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=openid%20profile
&state=random_state_string
&code_challenge=BASE64URL_SHA256_HASH
&code_challenge_method=S256POST /api/oauth/token — Refresh a token
Accepts authorization_code and rotating refresh_token grants as JSON or URL-encoded scalar form fields. Public PKCE grants must omit client_secret entirely; empty or incorrect secrets are rejected. A refresh request may narrow the granted scopes, but cannot add scopes.
Access tokens expire after 1 hour; refresh tokens expire after 30 days. Save both new tokens returned by each refresh.
// Authentication follows the original grant, not a guessed client type.
const response = await fetch('https://www.cpoauth.com/api/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: 'YOUR_REFRESH_TOKEN',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET' // omit only when the original PKCE exchange omitted it
})
})
// Returns new access_token + new refresh_token (rotation)
const { access_token, refresh_token, expires_in } = await response.json()GET /api/oauth/userinfo
Returns only the user data covered by the granted scopes.
// Fetch user data with access token
const userinfo = await fetch('https://www.cpoauth.com/api/oauth/userinfo', {
headers: { Authorization: 'Bearer {access_token}' }
})
const data = await userinfo.json()
// Response varies based on granted scopes:
// {
// sub,
// username,
// display_name,
// avatar_url,
// bio,
// email,
// email_verified,
// linked_accounts,
// cp_summary,
// cp_details
// }POST /api/oauth/revoke
Revokes your application's access or refresh token (RFC 7009). Secret authentication follows the original grant. Revoking a refresh token also invalidates all access tokens for that user and application. A valid request returns 200 for an unknown token; token_type_hint only affects lookup order.
// Revoke a token (RFC 7009)
await fetch('https://www.cpoauth.com/api/oauth/revoke', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET', // omit only for a grant originally exchanged without it
token: 'TOKEN_TO_REVOKE',
token_type_hint: 'refresh_token' // or 'access_token'
})
})
// Validly authenticated requests return 200 even for an unknown or already revoked token.
// JSON and application/x-www-form-urlencoded are supported; Basic authentication is not.Permission scopes
Request a non-empty set of only the scopes your application needs. The openid scope is optional. The userinfo response follows stored granted scopes, independently of public-profile privacy settings.
| Scope | Returned data |
|---|---|
openid | User ID (sub only; no ID token) |
profile | Username, display name, avatar URL and bio |
email | Email address and verification status |
cp:linked | All linked platform accounts (platform, UID and username) |
link:luogu | Linked Luogu account (UID and username) |
link:atcoder | Linked AtCoder account (UID and username) |
link:leetcode | Linked LeetCode account (UID and username) |
link:codeforces | Linked Codeforces account (UID and username) |
link:github | Linked GitHub account (UID and username) |
link:google | Linked Google account (UID and username) |
link:clist | Linked Clist.by account (UID and username) |
cp:summary | Rating, contest count and rank from Clist.by for accounts that exactly match locally linked identities |
cp:details | Up to 200 recent rated contest changes from Clist.by for accounts that exactly match locally linked identities |
User profile card
Embed an SVG card of a user's public platform accounts in a GitHub README or blog. The card shows only accounts the user has made public.
Endpoint:GET /api/users/{username}/card.svg
Parameters: width (default 480, range 300–800), theme (light or dark, default light), lang (en/zh/ja, default en).
Markdown example

<!-- Dark theme -->

<!-- Custom width -->
