Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.flexslot.gg/llms.txt

Use this file to discover all available pages before exploring further.

The Authorization Code Grant with PKCE is the only user-facing grant Flexslot supports. This page is the complete reference: every parameter, every header, every status code.

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 original code_verifier.
  • Flexslot recomputes SHA256(code_verifier) and compares to the stored challenge.
If the values don’t match, you get invalid_grant and the code is invalidated. See RFC 7636 for the spec.
code_challenge_method=plain is forbidden at Flexslot. Use S256 only. RFC 9700 deprecates plain because it provides no actual protection.

Generating PKCE values

import crypto from 'node:crypto'

function base64url(buffer) {
  return buffer
    .toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '')
}

export function generatePkce() {
  const verifier = base64url(crypto.randomBytes(32))
  const challenge = base64url(crypto.createHash('sha256').update(verifier).digest())
  return { verifier, challenge, method: 'S256' }
}

Step 1 — Authorization request

Send the user’s browser to:
GET https://api.flexslot.gg/api/public/v1/oauth/authorize

Required query parameters

ParameterValueDescription
response_typecodeThe only supported value.
client_idcli_…The client ID from the partner admin.
redirect_uriFull URLMust exactly match a URI registered for this client.
scopeSpace-separatedE.g. decks:read sideboards:write. See Scopes.
stateRandom stringCSRF token. You’ll validate it on the callback.
code_challengeBase64url SHA-256From the PKCE generator above.
code_challenge_methodS256Required. plain is forbidden.

Example

https://api.flexslot.gg/api/public/v1/oauth/authorize?
  response_type=code&
  client_id=cli_01HX2K…&
  redirect_uri=https%3A%2F%2Fyour.app%2Foauth%2Fcallback&
  scope=decks%3Aread%20sideboards%3Aread&
  state=4f8a9c1e2b3d…&
  code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
  code_challenge_method=S256
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
If the user clicks Allow, you get redirected with a code. If they click Deny, you get redirected with error=access_denied.

Step 3 — Callback to your redirect_uri

GET https://your.app/oauth/callback?
  code=01HX2K6F8N9P…&
  state=4f8a9c1e2b3d…&
  iss=https://api.flexslot.gg

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

POST https://api.flexslot.gg/api/public/v1/oauth/token
Content-Type: application/x-www-form-urlencoded

Body parameters

ParameterRequired?Description
grant_typeYesauthorization_code
codeYesThe code from the callback
redirect_uriYesSame URI you used in step 1
code_verifierYesThe PKCE verifier you generated in step 1
client_idIf public clientThe client ID (confidential clients send it via Basic auth)

Authentication

Client typeHow to authenticate
Confidential (has client_secret)Authorization: Basic <base64(client_id:client_secret)>
Public (no secret)Include client_id in the body

Example request

POST /api/public/v1/oauth/token HTTP/1.1
Host: api.flexslot.gg
Content-Type: application/x-www-form-urlencoded
Authorization: Basic Y2xpXzAxSFgyS…OnNlY3JldA==

grant_type=authorization_code
&code=01HX2K6F8N9P…
&redirect_uri=https%3A%2F%2Fyour.app%2Foauth%2Fcallback
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Success response

200 OK, Content-Type: application/json, Cache-Control: no-store:
{
  "access_token": "fst_at_01HX2K6F8N9PRSTUVWXYZA…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "fst_rt_01HX2K6F8N9PRSTUVWXYZB…",
  "scope": "decks:read sideboards:read"
}
FieldDescription
access_tokenSend as Authorization: Bearer <token> on API requests.
token_typeAlways Bearer (or DPoP if you opted in — see DPoP).
expires_inSeconds until the access token expires. Currently 3600.
refresh_tokenUse to mint new access tokens. Rotates on every use.
scopeThe scopes actually granted. May be narrower than what you requested.

Error response

400 Bad Request (or 401 for invalid_client), Content-Type: application/json:
{
  "error": "invalid_grant",
  "error_description": "The authorization code is expired or has already been used."
}
See Errors for the full catalog.

Step 5 — Call the API

GET /api/public/v1/decks/ HTTP/1.1
Host: api.flexslot.gg
Authorization: Bearer fst_at_01HX2K6F8N9PRSTUVWXYZA…
The resource server returns 401 Unauthorized with a WWW-Authenticate header if the token is invalid, expired, or doesn’t have the right scope:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="flexslot",
  error="insufficient_scope",
  error_description="Requires scope: decks:write",
  scope="decks:write"

Step 6 — Refresh

When the access token expires (or proactively, ~5 minutes before it does), exchange the refresh token for a new pair:
POST /api/public/v1/oauth/token HTTP/1.1
Host: api.flexslot.gg
Content-Type: application/x-www-form-urlencoded
Authorization: Basic Y2xpXzAxSFgyS…OnNlY3JldA==

grant_type=refresh_token
&refresh_token=fst_rt_01HX2K6F8N9PRSTUVWXYZB…
The response shape is identical to the initial exchange — and includes a new refresh token. Store the new one. The old one is now invalid.
Refresh tokens rotate. If you replay a refresh token after it’s been used, Flexslot revokes the entire grant. The user will have to re-consent. See Security → Refresh token rotation.

Common gotchas

SymptomCauseFix
invalid_grant on first exchangeCode already used, or expired (60s)Restart the flow
invalid_grant with “code_verifier mismatch”Verifier doesn’t match the challengeMake sure you stored the verifier from the same session
invalid_request with “redirect_uri mismatch”Different URI in step 1 vs step 4They must be byte-for-byte identical
invalid_client (401)Wrong client_id, wrong secret, or wrong auth methodCheck the partner admin
unsupported_grant_typeYou sent something other than authorization_code or refresh_tokenThose are the only two grants supported
Browser stuck on /authorizeredirect_uri isn’t registered for this clientAdd it in the partner admin

Code samples

Full Express, Flask, and curl implementations

Security

PKCE, state, redirect URIs in depth

Errors

Every error code explained

DPoP

Sender-constrained tokens