CorrConnectDocs
Early accessRequest access

Concepts

OAuth 2.0 with PKCE

CorrConnect speaks standard OAuth 2.0 authorization code with PKCE (RFC 6749 and RFC 7636), plus RFC 7009 token revocation. Most OAuth libraries work with it as a public client.

The flow

Authorization code flow with PKCE1. Athlete's browser to Your app: Clicks Connect. 2. Your app to Athlete's browser: Redirect to /oauth/authorize with code_challenge and state. 3. Athlete's browser to CorrConnect: Signs in and approves the scopes. 4. CorrConnect to Your app: Redirect to your callback with code and state. 5. Your app to CorrConnect: POST /oauth/token with code and code_verifier. 6. CorrConnect to Your app: Access token and refresh token. 7. Your app to CorrConnect: GET /v1/activities with the Bearer token. 8. CorrConnect to Your app: Later: signed activity.created webhook.Athlete's browserYour appCorrConnectClicks Connect1Redirect to /oauth/authorize with code_challenge and state2Signs in and approves the scopes3Redirect to your callback with code and state4POST /oauth/token with code and code_verifier5Access token and refresh token6GET /v1/activities with the Bearer token7Later: signed activity.created webhook8
  1. Your app makes a code_verifier: 43 to 128 characters from A-Z a-z 0-9 - . _ ~, random each time.
  2. It sends the athlete to /oauth/authorize with code_challenge = BASE64URL(SHA256(code_verifier)), no padding, and code_challenge_method=S256. Plain challenges are not accepted.
  3. The athlete signs in and approves. The browser lands on your redirect_uri with code and your state.
  4. Your app posts the code, the verifier and the same redirect_uri to /oauth/token.
  5. It gets an access token and a refresh token.

CorrConnect checks the authorize request before it shows anything. A wrong client_id, an unregistered callback, an unknown scope, a state shorter than 8 characters or a malformed challenge stops there with a 400 error in the athlete's browser, and nothing is sent to your callback. If the athlete denies, your callback gets error=access_denied and your state.

Codes

An authorization code:

  • lasts 5 minutes,
  • works once,
  • only works for the app, callback, challenge and scopes it was issued for.

If a code is presented a second time, CorrConnect assumes someone copied it and withdraws every token the first exchange produced. Don't retry an exchange automatically; if you lost the response, start a new authorization.

Tokens

TokenPrefixDefault lifetimeNotes
Access tokencca_60 minutesSend as Authorization: Bearer. Read expires_in from each response; the default can change.
Refresh tokenccr_60 daysWorks once. Each refresh returns a new pair.
Refresh chain365 daysCounted from the first code exchange. refresh_token_expires_in counts down to whichever comes first.

Both are opaque strings; don't parse them. CorrConnect stores only keyed hashes, so it can't show you a token again or tell you what one was. Keep them out of URLs, logs and analytics.

Treat refresh_token and refresh_token_expires_in as optional in your parser. An app that ignores them keeps working: it sends the athlete through authorization again when the access token expires.

Refreshing

Post grant_type=refresh_token, your client_id and the current refresh token. You get a new access token and a new refresh token. Save the new refresh token before you use the new access token; the old one is spent.

Refresh tokens are single use, and CorrConnect watches for a spent one coming back:

  • Within 10 seconds of the first use (two of your workers raced), the second request gets its own new pair and the pair from the first request is withdrawn. Keep whichever response you saved last.
  • After that, a spent refresh token is treated as stolen. CorrConnect ends the whole grant, sends grant.revoked, and the athlete has to approve your app again.

The safe pattern is one refresh at a time per athlete: a lock or a queue keyed on the athlete.

You can ask for fewer scopes on refresh with scope=activities:read. Asking for a scope the grant doesn't hold fails with invalid_grant, the same answer as an expired or revoked token, so the error never tells anyone whether a token is still alive.

Token endpoint errors

/oauth/token uses the OAuth error shape instead of CorrConnect's usual one:

JSON
{"error": "invalid_grant", "error_description": "The authorization code is invalid, expired, used, or revoked.", "request_id": "req_..."}

The possible values are invalid_request, invalid_client, invalid_grant, unsupported_grant_type and, with 429, slow_down. See errors.

Revoking

  • POST /oauth/revoke with the access token as a Bearer header ends the athlete's whole grant for your app. Use it when they disconnect inside your app. It works with an expired access token too.
  • POST /oauth/revoke-token releases one token and its partner (the access token and the refresh token issued together). The grant stays. It takes a Bearer header, or an RFC 7009 form body with token.

Both answer {"status": "revoked"} whether or not the token existed.

Expired access tokens are deleted 7 days after they expire, except the newest one for each active grant. That one is kept so an app without a refresh token can still end the grant with /oauth/revoke.

Using an OAuth library

Configure it as a public client with PKCE (S256), the authorization endpoint CORRCONNECT_BASE/oauth/authorize, the token endpoint CORRCONNECT_BASE/oauth/token, client authentication none (send client_id in the body), and scopes separated by spaces. CorrConnect publishes no OpenID Connect discovery document, ID tokens or userinfo endpoint; use GET /v1/profile for a stable athlete ID.