OAuth & Sign in with Sporty

The authorization code flow

The whole flow with real requests, from sending someone to Sporty to refreshing a token.

Authorization code with PKCE, which is the flow for any application acting on behalf of a person. Every example below is a real request.

1. Make a PKCE verifier and challenge

PKCE is required, for confidential clients too, and only S256 is accepted — plain is refused.

VERIFIER=$(openssl rand -base64 60 | tr -d '\n=+/' | cut -c1-64)
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')

Keep the verifier; you need it in step 4.

2. Send the member to Sporty

https://api.sporty.plus/oauth2/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.example/callback
  &scope=openid%20profile%20email%20club.courts.read
  &state=RANDOM_PER_REQUEST
  &code_challenge=CHALLENGE
  &code_challenge_method=S256
ParameterNotes
stateRandom, unguessable, per request. Check it on the way back — that is what makes the callback yours.
nonceOptional. If you send it, it comes back in the id_token. Use it if you want one.
club_idOptional. Only meaningful for a client granted access to several clubs, to say which one this is about.

The member sees a consent screen on my.sporty.plus listing, in plain language, exactly what you asked for. They can approve less than you asked for; grant requests are never widened.

3. They come back to your redirect URI

https://yourapp.example/callback?code=...&state=...

If they declined, or something was wrong:

https://yourapp.example/callback?error=access_denied&error_description=...&state=...

Compare state with what you sent before doing anything else.

4. Exchange the code

Within a few minutes, and once only. A code is single use.

curl -X POST https://api.sporty.plus/oauth2/token \
  -H 'Accept: application/json' \
  -d grant_type=authorization_code \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_SECRET \
  -d redirect_uri=https://yourapp.example/callback \
  -d code_verifier="$VERIFIER" \
  -d code=THE_CODE

Omit client_secret for a public client.

{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJ0eXAiOiJKV1Qi...",
  "refresh_token": "def5020089ab...",
  "id_token": "eyJ0eXAiOiJKV1Qi..."
}

id_token is present only when you asked for openid. See Sign in with Sporty.

5. Use it

curl https://api.sporty.plus/api/partner/v1/clubs/me \
  -H "Authorization: Bearer $ACCESS_TOKEN"

6. Refresh

Access tokens last an hour; refresh tokens thirty days (a day for a self-registered client).

curl -X POST https://api.sporty.plus/oauth2/token \
  -H 'Accept: application/json' \
  -d grant_type=refresh_token \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_SECRET \
  -d refresh_token=THE_REFRESH_TOKEN

7. Revoke, when you are done

curl -X POST https://api.sporty.plus/oauth2/token/revoke \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_SECRET \
  -d token=THE_ACCESS_TOKEN

This always answers 200, whatever you send — that is what RFC 7009 requires, so the endpoint cannot be used to find out whether a token is valid.

Name the access token, not the refresh token. Revoking an access token also revokes the refresh tokens issued with it, which is what "end this session" means in practice. A refresh token on its own cannot currently be named here.

A member can also revoke you at any moment, from Authorised applications in their profile. When that happens every token you hold stops working immediately, and you should send them through the flow again rather than retrying.

Errors you should expect

ErrorWhat happened
invalid_requestUsually a missing or plain code_challenge. PKCE with S256 is required.
invalid_scopeA scope that does not exist, or one this kind of client may not ask for.
invalid_grantThe code was already spent, expired, or the code_verifier does not match.
access_deniedThe member said no, or the application is suspended.
insufficient_scope (403)The token is valid but was not granted this scope. The WWW-Authenticate header names what is needed.

An unregistered redirect_uri is never redirected to. You will see the error on Sporty instead — which is the point.