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 https URLs (http://localhost allowed 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

GET /oauth/authorize
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=S256

The 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 the redirect_uri they 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 state matches 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

POST /oauth/token — authorization_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"
}
TokenLifetimeNotes
access_token6 hoursBearer token for /api/v1 requests.
refresh_tokenUp to 180 daysSingle use — each refresh revokes it and issues a new pair (rotation).

4. Refresh

POST /oauth/token — refresh_token
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

POST /oauth/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 200 even 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": "…" }:

ErrorStatusMeaning
invalid_request400A required parameter is missing (e.g. code, refresh_token).
invalid_client401Unknown client_id, wrong client_secret, or the app is suspended.
invalid_grant400Code/refresh token is expired, revoked, already used, belongs to another app, or PKCE/redirect_uri validation failed.
unsupported_grant_type400grant_type must be authorization_code or refresh_token.