Authentication
OAuth apps
For apps acting on behalf of other Gravl users. Standard OAuth 2.0 authorization-code with PKCE: users grant your app specific scopes, you get short-lived access tokens with a rotating refresh token, and the grant is revocable at any time.
1. Register your app
Registration is manual while the API is in beta. Email developers@gravl.ai with:
- Your app’s name and a short description of what it does
- Your OAuth redirect URI(s) — absolute
httpsURLs (http://localhostallowed for development) - The scopes your app needs — request the minimum; users see exactly what you ask for
You’ll receive a client_id (gci_…) and a client_secret (gcs_…, shown once — store it like a password).
2. Send the user to the consent screen
https://api.gravl.ai/oauth/authorize
?client_id=gci_…
&redirect_uri=https://yourapp.example/callback // must be registered exactly
&scope=workouts:read stats:read // omit for all allowed scopes
&state=<random value you verify on return>
&code_challenge=<S256(code_verifier)>
&code_challenge_method=S256The user signs in with their Gravl account, sees your app’s name and the requested scopes in plain language, and approves — their browser comes back to your redirect_uri with ?code=gac_…&state=… (or error=access_denied if they cancel).
- Codes are prefixed
gac_, valid for 10 minutes, single use, and bound to your app and theredirect_urithey were issued for - PKCE (S256) is required for public clients (browser or native apps, which have no
client_secret) and strongly recommended for everyone - Always verify
statematches what you sent — it’s your CSRF protection
Public clients omit client_secret at the token endpoint — PKCE is their client authentication. Tell us which type your app is when registering.
3. Exchange the code
curl -X POST https://api.gravl.ai/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code=gac_… \
-d client_id=gci_… \
-d client_secret=gcs_… \
-d code_verifier=<your PKCE verifier> \
-d redirect_uri=https://yourapp.example/callback
{
"access_token": "gat_…",
"token_type": "Bearer",
"expires_in": 21600,
"refresh_token": "grt_…",
"scope": "workouts:read stats:read"
}| Token | Lifetime | Notes |
|---|---|---|
access_token | 6 hours | Bearer token for /api/v1 requests. |
refresh_token | Up to 180 days | Single use — each refresh revokes it and issues a new pair (rotation). |
4. Refresh
curl -X POST https://api.gravl.ai/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=grt_… \
-d client_id=gci_… \
-d client_secret=gcs_…The response has the same shape as the code exchange. The presented refresh token is revoked in the same transaction — always persist the new refresh_token before discarding the old one. Replaying a used refresh token returns invalid_grant.
5. Revoke
curl -X POST https://api.gravl.ai/oauth/revoke \
-d token=grt_… \
-d client_id=gci_… \
-d client_secret=gcs_…- Revoking an access token kills that token only
- Revoking a refresh token revokes the entire grant — every outstanding token for that user + app pair
- Per RFC 7009 the endpoint returns
200even for unknown tokens — only bad client credentials produce an error - Users can also revoke your app’s access from their account
Token endpoint errors
Errors follow RFC 6749 — { "error": "…", "error_description": "…" }:
| Error | Status | Meaning |
|---|---|---|
invalid_request | 400 | A required parameter is missing (e.g. code, refresh_token). |
invalid_client | 401 | Unknown client_id, wrong client_secret, or the app is suspended. |
invalid_grant | 400 | Code/refresh token is expired, revoked, already used, belongs to another app, or PKCE/redirect_uri validation failed. |
unsupported_grant_type | 400 | grant_type must be authorization_code or refresh_token. |