API reference
OAuth endpoints
Start the athlete's authorization, swap codes and refresh tokens, and give access back. The concepts are in OAuth 2.0 with PKCE.
Ask an athlete for access
GET/oauth/authorize
Browser page, not an API call. Send the athlete here; CorrConnect checks every value before it shows sign-in. After the athlete decides, the browser goes to redirect_uri with code and state (approved) or error=access_denied and state (denied). A request that fails the checks gets a 400 error page and is not redirected.
- Auth
- None
- Host
- Portal origin only
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
response_type required | query | string | Always code. One of code |
client_id required | query | string | Your app's client_id. |
redirect_uri required | query | string | One of the app's registered callbacks, character for character. |
scope required | query | string | Space-separated scopes, for example activities:read profile:read. |
state required | query | string | Random value of at least 8 characters; it comes back unchanged. at least 8 characters |
code_challenge required | query | string | BASE64URL(SHA256(code_verifier)) without padding (43 to 128 characters). 43 to 128 characters |
code_ required | query | string | Always S256. One of S256 |
Response 200
The HTML sign-in and consent page. After the athlete decides, the page sends the browser to redirect_uri with code and state, or error=access_denied and state.
Errors
| Status | Code | When |
|---|---|---|
| 400 | unsupported_ | response_type is not code or code_challenge_method is not S256. |
| 400 | invalid_ | state is shorter than 8 characters or code_challenge is malformed. |
| 400 | invalid_client | No app has this client_id. |
| 400 | redirect_ | redirect_uri is not one of the app's registered callbacks, exactly. |
| 400 | invalid_scope | A requested scope is unknown or the scope list is empty. |
CORRCONNECT_BASE/oauth/authorize
?response_type=code
&client_id=app_0123456789abcdef0123456789abcdef
&redirect_uri=https%3A%2F%2Fdeveloper.example%2Fcallback
&scope=activities%3Aread%20profile%3Aread
&state=<random, 8+ characters>
&code_challenge=<BASE64URL(SHA256(code_verifier))>
&code_challenge_method=S256Exchange a code or refresh a token
POST/oauth/token
Public-client token endpoint; no client secret. grant_type=authorization_code exchanges a one-use code with its PKCE verifier. grant_type=refresh_token sends client_id and refresh_token (and optionally a narrower scope); each refresh token works once and the response carries its replacement. Presenting a used refresh token again revokes the whole grant.
- Auth
- None
- Host
- Portal origin or API host
Request body application/x-www-form-urlencoded
| Field | Type | Notes |
|---|---|---|
grant_type required | string | authorization_code or refresh_token. |
client_id required | string | Your app's client_id. |
code | string or null | authorization_code only: the one-use code from your callback. |
redirect_uri | string or null | authorization_code only: the same redirect_uri you sent to /oauth/authorize. |
code_verifier | string or null | authorization_code only: the PKCE verifier behind the code_challenge. |
refresh_token | string or null | refresh_token only: your current refresh token. |
scope | string or null | refresh_token only, optional: ask for fewer scopes than the grant holds. |
Response 200
One-hour default bearer token; actual lifetime is expires_in seconds.
| Field | Type | Notes |
|---|---|---|
access_token required | string | |
token_type required | string | One of Bearer |
expires_in required | integer | |
scope required | string | |
refresh_token | string | |
refresh_ | integer |
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | A required field is missing or malformed. |
| 400 | invalid_client | client_id is not a CorrConnect app ID. |
| 400 | invalid_grant | The code or refresh token is invalid, expired, used or revoked, the verifier or redirect_uri does not match, or a refresh asked for scopes the grant lacks. |
| 400 | unsupported_ | grant_type is not authorization_code or refresh_token. |
| 429 | slow_down | Too many failed attempts, or too many token requests from one address. |
curl "$CORRCONNECT_BASE/oauth/token" \
-d grant_type=authorization_code \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=$REDIRECT_URI" \
--data-urlencode "code_verifier=$CODE_VERIFIER"const response = await fetch(`${process.env.CORRCONNECT_BASE}/oauth/token`, {
method: "POST",
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: process.env.CLIENT_ID,
code: process.env.CODE,
redirect_uri: process.env.REDIRECT_URI,
code_verifier: process.env.CODE_VERIFIER,
}),
});
console.log(response.status, await response.json());import os
import requests
response = requests.post(
f"{os.environ['CORRCONNECT_BASE']}/oauth/token",
data={
"grant_type": "authorization_code",
"client_id": os.environ["CLIENT_ID"],
"code": os.environ["CODE"],
"redirect_uri": os.environ["REDIRECT_URI"],
"code_verifier": os.environ["CODE_VERIFIER"],
},
timeout=10,
)
print(response.status_code, response.json())import Foundation
let env = ProcessInfo.processInfo.environment
var request = URLRequest(url: URL(string: "\(env["CORRCONNECT_BASE"]!)/oauth/token")!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
var form = URLComponents()
form.queryItems = [
URLQueryItem(name: "grant_type", value: "authorization_code"),
URLQueryItem(name: "client_id", value: env["CLIENT_ID"]!),
URLQueryItem(name: "code", value: env["CODE"]!),
URLQueryItem(name: "redirect_uri", value: env["REDIRECT_URI"]!),
URLQueryItem(name: "code_verifier", value: env["CODE_VERIFIER"]!),
]
request.httpBody = form.percentEncodedQuery!.replacingOccurrences(of: "+", with: "%2B").data(using: .utf8)
let (data, response) = try await URLSession.shared.data(for: request)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self)){
"access_token": "<opaque-access-token>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "activities:read profile:read",
"refresh_token": "<opaque-refresh-token>",
"refresh_token_expires_in": 5184000
}Revoke one token pair
POST/oauth/
Releases one token and the token issued with it; the grant stays active. Send the access token as a Bearer header, or a form body with token (access or refresh), optionally token_type_hint and client_id, as in RFC 7009.
- Auth
- Bearer access token
- Host
- Portal origin or API host
Request body application/x-www-form-urlencoded
| Field | Type | Notes |
|---|---|---|
token | string or null | An access or refresh token, if you are not sending a Bearer header. |
token_type_hint | string or null | Optional: access_token or refresh_token. CorrConnect checks both either way. |
client_id | string or null | Optional: when sent, a token from another app is ignored. |
Response 200
Idempotent release; token existence is not disclosed.
| Field | Type | Notes |
|---|---|---|
status required | string | One of revoked |
Errors
| Status | Code | When |
|---|---|---|
| 401 | invalid_ | Neither a Bearer header nor a token field was sent. |
| 422 | validation_ | A field is missing, has the wrong type or is out of range; details.fields says which. |
curl -X POST "$CORRCONNECT_BASE/oauth/revoke-token" \
-H "Authorization: Bearer $ACCESS_TOKEN"const response = await fetch(`${process.env.CORRCONNECT_BASE}/oauth/revoke-token`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.ACCESS_TOKEN}`,
},
});
console.log(response.status, await response.json());import os
import requests
response = requests.post(
f"{os.environ['CORRCONNECT_BASE']}/oauth/revoke-token",
headers={"Authorization": f"Bearer {os.environ['ACCESS_TOKEN']}"},
timeout=10,
)
print(response.status_code, response.json())import Foundation
let env = ProcessInfo.processInfo.environment
var request = URLRequest(url: URL(string: "\(env["CORRCONNECT_BASE"]!)/oauth/revoke-token")!)
request.httpMethod = "POST"
request.setValue("Bearer \(env["ACCESS_TOKEN"]!)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self)){
"status": "revoked"
}End the whole grant
POST/oauth/revoke
Send the access token as a Bearer header. The athlete's grant for your app ends with every code and token in it, and subscribed apps get grant.revoked. Expired tokens still work here; unknown tokens get the same answer.
- Auth
- Bearer access token
- Host
- Portal origin or API host
Response 200
Idempotent release; token existence is not disclosed.
| Field | Type | Notes |
|---|---|---|
status required | string | One of revoked |
Errors
| Status | Code | When |
|---|---|---|
| 401 | invalid_ | No Bearer header was sent. |
curl -X POST "$CORRCONNECT_BASE/oauth/revoke" \
-H "Authorization: Bearer $ACCESS_TOKEN"const response = await fetch(`${process.env.CORRCONNECT_BASE}/oauth/revoke`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.ACCESS_TOKEN}`,
},
});
console.log(response.status, await response.json());import os
import requests
response = requests.post(
f"{os.environ['CORRCONNECT_BASE']}/oauth/revoke",
headers={"Authorization": f"Bearer {os.environ['ACCESS_TOKEN']}"},
timeout=10,
)
print(response.status_code, response.json())import Foundation
let env = ProcessInfo.processInfo.environment
var request = URLRequest(url: URL(string: "\(env["CORRCONNECT_BASE"]!)/oauth/revoke")!)
request.httpMethod = "POST"
request.setValue("Bearer \(env["ACCESS_TOKEN"]!)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self)){
"status": "revoked"
}