Get started
Quickstart
You'll play both parts here, the developer and the athlete, so you can see the whole flow end to end. You need a CorrConnect account, a terminal, and one of Node.js 20, Python 3.10 (with pip install requests) or Swift 5.9.
1. Register an app
Sign in to CorrConnect at your CORRCONNECT_BASE address and open Developer workspace. Give the app a name (athletes see it on the consent screen) and one callback URL. The callback has to be an exact https:// URL; it does not need to be running yet for this walkthrough.
You get back a client_id that starts with app_. That's all you need: CorrConnect apps have no client secret, so there is nothing to hide in your code or config.
Put the values in your shell. Every sample in these docs reads them from the environment.
export CORRCONNECT_BASE="https://corrconnect.example" # your CorrConnect address
export CLIENT_ID="app_0123456789abcdef0123456789abcdef"
export REDIRECT_URI="https://developer.example/callback"2. Make a PKCE pair and the authorize link
PKCE replaces the client secret. You make a random code_verifier, keep it, and send its SHA-256 hash (the code_challenge) with the link. Only someone holding the verifier can redeem the code later.
const base64url = (bytes) =>
btoa(String.fromCharCode(...new Uint8Array(bytes)))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = base64url(
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)),
);
const state = base64url(crypto.getRandomValues(new Uint8Array(16)));
const url = new URL("/oauth/authorize", process.env.CORRCONNECT_BASE);
url.search = new URLSearchParams({
response_type: "code",
client_id: process.env.CLIENT_ID,
redirect_uri: process.env.REDIRECT_URI,
scope: "activities:read profile:read",
state,
code_challenge: challenge,
code_challenge_method: "S256",
}).toString();
console.log(`export CODE_VERIFIER="${verifier}"`);
console.log(`export STATE="${state}"`);
console.log(url.toString());import base64
import hashlib
import os
import secrets
from urllib.parse import urlencode
verifier = secrets.token_urlsafe(48)
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
state = secrets.token_urlsafe(16)
url = os.environ["CORRCONNECT_BASE"] + "/oauth/authorize?" + urlencode({
"response_type": "code",
"client_id": os.environ["CLIENT_ID"],
"redirect_uri": os.environ["REDIRECT_URI"],
"scope": "activities:read profile:read",
"state": state,
"code_challenge": challenge,
"code_challenge_method": "S256",
})
print(f'export CODE_VERIFIER="{verifier}"')
print(f'export STATE="{state}"')
print(url)import CryptoKit
import Foundation
import Security
func base64URL(_ data: Data) -> String {
data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
var bytes = [UInt8](repeating: 0, count: 32)
guard SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) == errSecSuccess else { fatalError("no randomness") }
let verifier = base64URL(Data(bytes))
let challenge = base64URL(Data(SHA256.hash(data: Data(verifier.utf8))))
let state = UUID().uuidString
let env = ProcessInfo.processInfo.environment
var components = URLComponents(string: env["CORRCONNECT_BASE"]! + "/oauth/authorize")!
components.queryItems = [
URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "client_id", value: env["CLIENT_ID"]!),
URLQueryItem(name: "redirect_uri", value: env["REDIRECT_URI"]!),
URLQueryItem(name: "scope", value: "activities:read profile:read"),
URLQueryItem(name: "state", value: state),
URLQueryItem(name: "code_challenge", value: challenge),
URLQueryItem(name: "code_challenge_method", value: "S256"),
]
print("export CODE_VERIFIER=\"\(verifier)\"")
print("export STATE=\"\(state)\"")
print(components.url!.absoluteString)Run it, paste the two export lines into your shell, and open the link.
3. Approve it as the athlete
The link opens CorrConnect's consent screen. Sign in with your account, check the app name and the two scopes, and approve. Your browser then goes to your callback:
https://developer.example/callback?code=ccc_...&state=...If nothing is listening there, the page fails to load, which is fine: copy code from the address bar. Check that state matches the value you exported, then:
export CODE="ccc_..." # from the address bar; it expires in 5 minutes4. Swap the code for tokens
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(
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 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"]!),
]
var request = URLRequest(url: URL(string: env["CORRCONNECT_BASE"]! + "/oauth/token")!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
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))A successful exchange looks like this:
{
"access_token": "cca_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "activities:read profile:read",
"refresh_token": "ccr_...",
"refresh_token_expires_in": 5184000
}export ACCESS_TOKEN="cca_..."5. Make your first call
curl "$CORRCONNECT_BASE/v1/activities?limit=5" \
-H "Authorization: Bearer $ACCESS_TOKEN"const response = await fetch(`${process.env.CORRCONNECT_BASE}/v1/activities?limit=5`, {
headers: { Authorization: `Bearer ${process.env.ACCESS_TOKEN}` },
});
console.log(response.status, await response.json());import os
import requests
response = requests.get(
os.environ["CORRCONNECT_BASE"] + "/v1/activities",
params={"limit": 5},
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"]! + "/v1/activities?limit=5")!)
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))A new account has no activities yet, so expect {"data": [], "next_cursor": null}. Add one as the athlete (upload a FIT, GPX or TCX file in the portal) and call again:
{
"data": [
{
"id": "act_0123456789abcdef0123456789abcdef",
"source": "fit",
"activity_type": "ride",
"started_at": "2026-10-11T07:02:11Z",
"duration_seconds": 5412.0,
"duration_basis": "recorded_session_total_elapsed_time",
"distance_meters": 41870.2,
"distance_basis": "recorded_session_total_distance",
"provenance": {"format": "FIT", "file_type": "activity", "session_index": 1},
"laps": [
{
"index": 1,
"started_at": "2026-10-11T07:02:11Z",
"ended_at": "2026-10-11T08:32:23Z",
"duration_seconds": 5412.0,
"duration_basis": "recorded_lap_total_elapsed_time",
"distance_meters": 41870.2,
"distance_basis": "recorded_lap_total_distance"
}
],
"created_at": "2026-10-11T09:40:03+00:00"
}
],
"next_cursor": null
}That's a working integration. The access token lasts 60 minutes; the refresh token gets you a new one without asking the athlete again.
Next steps
- Connect a user turns these steps into a real callback handler with token storage and refresh.
- Receive webhooks gets you
activity.createdas soon as a workout lands. - Scopes and grants explains what the athlete approved and how they take it back.