Skip to main content

OAuth2

OAuth2 lets athletes connect their existing Saturday accounts to your platform. Instead of creating new partner-scoped athletes, you can request access to an athlete’s real Saturday profile — with their explicit consent.

When to use OAuth2

ScenarioAuth method
You manage athlete profiles in your platformAPI Key — create athletes via the API
Athletes already have Saturday accounts and want to connectOAuth2 — athlete grants you access
You want access to an athlete’s full Saturday historyOAuth2 — requires athlete consent
Most partners start with API keys. Add OAuth2 when athletes tell you they already have Saturday accounts.

The flow

Your app → Saturday authorize → Athlete logs in → Consent → Code → Token exchange → API access
1

Redirect to authorize

Your app redirects the athlete to Saturday’s authorization page
2

Athlete authenticates

Athlete logs into their Saturday account (or creates one)
3

Athlete consents

Athlete sees what scopes you’re requesting and clicks “Allow”
4

Code redirect

Saturday redirects back to your app with an authorization code
5

Token exchange

Your server exchanges the code for access and refresh tokens
6

API calls

Use the access token as a Bearer token for API requests

Step 1: Generate PKCE challenge

Saturday requires PKCE (Proof Key for Code Exchange) for all OAuth2 flows. Generate a code verifier and challenge:
import hashlib
import base64
import secrets

# Generate code verifier (43-128 characters)
code_verifier = secrets.token_urlsafe(32)

# Generate code challenge (SHA-256 hash of verifier)
digest = hashlib.sha256(code_verifier.encode("utf-8")).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("utf-8")

# Store code_verifier in your session — you need it for token exchange
print(f"Verifier: {code_verifier}")
print(f"Challenge: {code_challenge}")

Step 2: Redirect to authorize

Build the authorization URL and redirect the athlete:
https://api.saturday.fit/v1/oauth/authorize?
  client_id=your_partner_id
  &redirect_uri=https://your-app.com/callback
  &response_type=code
  &scope=athlete:read activity:read nutrition:read
  &state=random_csrf_token
  &code_challenge=YOUR_CODE_CHALLENGE
  &code_challenge_method=S256
ParameterRequiredDescription
client_idYesYour partner ID
redirect_uriYesMust match a registered redirect URI
response_typeYesAlways code
scopeYesSpace-separated list of requested scopes
stateYesRandom string for CSRF protection
code_challengeYesPKCE challenge
code_challenge_methodYesAlways S256

Step 3: Handle the callback

After the athlete consents, Saturday redirects to your redirect_uri with a code:
https://your-app.com/callback?code=AUTH_CODE_HERE&state=random_csrf_token
Always verify the state parameter matches what you sent. This prevents CSRF attacks. If the athlete denies access:
https://your-app.com/callback?error=access_denied&error_description=The+user+denied+access&state=random_csrf_token

Step 4: Exchange code for tokens

import requests

response = requests.post(
    "https://api.saturday.fit/v1/oauth/token",
    data={
        "grant_type": "authorization_code",
        "code": auth_code,
        "redirect_uri": "https://your-app.com/callback",
        "client_id": "your_partner_id",
        "client_secret": "your_client_secret",
        "code_verifier": code_verifier,
    },
)

tokens = response.json()
access_token = tokens["access_token"]      # JWT, 1-hour expiry
refresh_token = tokens["refresh_token"]    # Opaque, 90-day expiry
expires_in = tokens["expires_in"]          # Seconds until access token expires

Step 5: Use the access token

The access token is a JWT that contains the partner ID, athlete UID, and granted scopes. Use it as a Bearer token:
curl -H "Authorization: Bearer ACCESS_TOKEN_JWT" \
  https://api.saturday.fit/v1/nutrition/calculate \
  -X POST -H "Content-Type: application/json" \
  -d '{"activity_type": "bike", "duration_min": 120, "intensity_level": 3}'
The API automatically scopes requests to the athlete who granted consent. You don’t need to pass an athlete_id — it’s in the JWT.

Step 6: Refresh expired tokens

Access tokens expire after 1 hour. Use the refresh token to get a new pair:
response = requests.post(
    "https://api.saturday.fit/v1/oauth/token",
    data={
        "grant_type": "refresh_token",
        "refresh_token": refresh_token,
        "client_id": "your_partner_id",
    },
)

new_tokens = response.json()
# Old refresh token is now invalid — save the new one
access_token = new_tokens["access_token"]
refresh_token = new_tokens["refresh_token"]
Refresh tokens are single-use. Each refresh rotates both the access and refresh token. The old refresh token is immediately invalidated. Always store the new refresh token.

Available scopes

ScopeAccess granted
athlete:readView athlete profile and settings
athlete:writeModify athlete profile and settings
activity:readView activities and prescriptions
activity:writeCreate, update, delete activities
nutrition:readCalculate nutrition prescriptions
ai:chatAI Coach conversations
Request only the scopes you need. Athletes are more likely to consent when the request is minimal.

Error scenarios

ScenarioResponse
Invalid client_id400 + “Unknown client_id”
Mismatched redirect_uri400 + “redirect_uri does not match”
Missing PKCE400 + “PKCE is required”
Expired auth code (>10 min)400 + “Authorization code has expired”
Reused auth code400 + all tokens revoked
Wrong code_verifier400 + “PKCE verification failed”
Expired access token401 + “access token has expired”
Revoked refresh token400 + “refresh token has been revoked”

Revoking tokens

Athletes can revoke access from their Saturday account settings. Partners can programmatically revoke:
POST /v1/oauth/token/revoke
Content-Type: application/x-www-form-urlencoded

token=REFRESH_TOKEN&client_id=your_partner_id