The flow at a glance
Why PKCE?
Without PKCE, anyone who intercepts the authorization code (browser history, server logs, a malicious app handling your custom URL scheme) can exchange it for tokens. PKCE binds the code to a secret your client generated before the flow started.- Your client generates
code_verifier— a random 43-128 character string. - Your client sends
code_challenge = BASE64URL(SHA256(code_verifier))to/authorize. - Flexslot stores the challenge against the issued code.
- When your client redeems the code at
/token, it sends the originalcode_verifier. - Flexslot recomputes
SHA256(code_verifier)and compares to the stored challenge.
invalid_grant and the code is invalidated. See RFC 7636 for the spec.
Generating PKCE values
Step 1 — Authorization request
Send the user’s browser to the consent screen on the Flexslot web app:This is
flexslot.gg, not api.flexslot.gg. The authorization endpoint is a web page the user’s browser opens — pointing the browser at the API host returns 401 FLEXSLOT_AUTHENTICATION_REQUIRED. Only the token, introspection, and revocation endpoints live on api.flexslot.gg.Required query parameters
Example
All query parameter values must be percent-encoded. Use your language’s URL builder — don’t hand-roll the string.
Step 2 — User consents
Flexslot renders the consent screen, showing the user:- Your application’s name and logo (from the partner admin)
- The scopes you requested, in human-readable form
- A clear Allow and Deny option
code. If they click Deny, you get redirected with error=access_denied.
Step 3 — Callback to your redirect_uri
What you must verify
1
state matches
Compare to the value you stored when starting the flow. If it doesn’t match, abort — this is a CSRF attempt.
2
iss equals https://api.flexslot.gg
RFC 9207 mix-up defense. If a different
iss shows up, you’ve been redirected to the wrong AS.3
No error parameter
If
error=... is present, the flow failed. See errors.Step 4 — Token exchange
Body parameters
Authentication
Example request
Success response
200 OK, Content-Type: application/json, Cache-Control: no-store:
Error response
400 Bad Request (or 401 for invalid_client), Content-Type: application/json:
Step 5 — Call the API
The resource server is game-scoped: deck, guide, and card endpoints live under/api/public/v1/games/{game}/..., where {game} is the game’s exact slug (for Magic, magic-the-gathering). There is no bare /api/public/v1/decks/ — it returns 404. The slug is the game field on any deck or guide you read back.
{ "error", "message", "details" }) on auth/scope failures — it does not emit an RFC 6750 WWW-Authenticate: Bearer challenge.
A token lacking the required scope returns 403 Forbidden:
401 Unauthorized with the matching error code:
WWW-Authenticate header is the DPoP lane (Authorization: DPoP …), which on proof failure returns 401 with WWW-Authenticate: DPoP error="invalid_dpop_proof".
Step 6 — Refresh
When the access token expires (or proactively, ~5 minutes before it does), exchange the refresh token for a new pair:Common gotchas
What to read next
Code samples
Full Express, Flask, and curl implementations
Security
PKCE, state, redirect URIs in depth
Errors
Every error code explained
DPoP
Sender-constrained tokens