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
| Scenario | Auth method |
|---|
| You manage athlete profiles in your platform | API Key — create athletes via the API |
| Athletes already have Saturday accounts and want to connect | OAuth2 — athlete grants you access |
| You want access to an athlete’s full Saturday history | OAuth2 — 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
Redirect to authorize
Your app redirects the athlete to Saturday’s authorization page
Athlete authenticates
Athlete logs into their Saturday account (or creates one)
Athlete consents
Athlete sees what scopes you’re requesting and clicks “Allow”
Code redirect
Saturday redirects back to your app with an authorization code
Token exchange
Your server exchanges the code for access and refresh tokens
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
| Parameter | Required | Description |
|---|
client_id | Yes | Your partner ID |
redirect_uri | Yes | Must match a registered redirect URI |
response_type | Yes | Always code |
scope | Yes | Space-separated list of requested scopes |
state | Yes | Random string for CSRF protection |
code_challenge | Yes | PKCE challenge |
code_challenge_method | Yes | Always 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
| Scope | Access granted |
|---|
athlete:read | View athlete profile and settings |
athlete:write | Modify athlete profile and settings |
activity:read | View activities and prescriptions |
activity:write | Create, update, delete activities |
nutrition:read | Calculate nutrition prescriptions |
ai:chat | AI Coach conversations |
Request only the scopes you need. Athletes are more likely to consent when the request is minimal.
Error scenarios
| Scenario | Response |
|---|
Invalid client_id | 400 + “Unknown client_id” |
Mismatched redirect_uri | 400 + “redirect_uri does not match” |
| Missing PKCE | 400 + “PKCE is required” |
| Expired auth code (>10 min) | 400 + “Authorization code has expired” |
| Reused auth code | 400 + all tokens revoked |
Wrong code_verifier | 400 + “PKCE verification failed” |
| Expired access token | 401 + “access token has expired” |
| Revoked refresh token | 400 + “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