Sign in with Shin ID
OAuth 2.0 authorization code with PKCE, two scopes, and a consent screen that names exactly what you receive.
Authorization code flow. PKCE for anything that cannot keep a secret (desktop, mobile, SPA); a client secret for a server that can. response_type=code is the only response type, and S256 the only challenge method — a request for anything else is refused rather than downgraded.
1. Send them to the consent screen
/oauth/authorizeNames your product, lists what it receives, and nothing more. If they are not signed in they land on sign-in first and come back here.
- client_idrequired
- Issued with your OAuth client.
- redirect_urirequired
- Must match one registered for the client, exactly. An unregistered URI is refused before consent, not after.
- scopeoptional
- Space separated.
identityandentitlementsare the only values; anything else is dropped. Default:identity entitlements. - staterecommended
- Echoed back untouched. Use it, and check it.
- code_challengePKCE
- Base64url SHA-256 of your verifier.
- code_challenge_methodPKCE
S256. No plain.- response_typeoptional
code, if sent.
GET /oauth/authorize
?client_id=gcl_…
&redirect_uri=https://yourapp.com/callback
&scope=identity%20entitlements
&state=<random, checked on return>
&code_challenge=<base64url(sha256(verifier))>
&code_challenge_method=S2562. Exchange the code
The code comes back on your redirect as ?code=gac_…&state=…. It lives five minutes and is single use — two concurrent exchanges of the same code cannot both succeed, by construction, so a retry that races itself gets invalid_grant rather than a second token.
/api/oauth/tokenAccepts form-encoded or JSON, and HTTP Basic for client_id:client_secret. Only grant_type=authorization_code.
curl -s -X POST "$GUIRU/api/oauth/token" \
-d grant_type=authorization_code \
-d code=gac_… \
-d redirect_uri=https://yourapp.com/callback \
-d client_id=gcl_… \
-d code_verifier=<the original verifier>{ "access_token": "gat_…", "token_type": "Bearer", "expires_in": 2592000, "scope": "identity entitlements" }3. Read who they are
/api/v1/megat_…Identity, plus the entitlement for your product when the token carries the entitlements scope.
{
"id": "3c1a…", // the Shin ID — this is your user_id everywhere else
"handle": "mario",
"display_name": "Mário",
"image": "https://…",
"entitlement": { // only with the entitlements scope
"product_id": "8f14…",
"active": true,
"type": "member",
"source": "member",
"ends_at": "2026-10-20T00:00:00.000Z"
}
}What the consent screen promises
It says, in these words: your Shin ID, handle and display name; whether you have access to this product; nothing else — no e-mail, no library, no taste. That is a promise the API keeps, so do not build a flow that needs an address you were never given.