CorrConnectDocs
Early accessRequest access

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.

Shell
export CORRCONNECT_BASE="https://corrconnect.example"   # your CorrConnect address
export CLIENT_ID="app_0123456789abcdef0123456789abcdef"
export REDIRECT_URI="https://developer.example/callback"

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.

authorize.mjs
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());
authorize.py
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)
authorize.swift
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:

Text
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:

Shell
export CODE="ccc_..."   # from the address bar; it expires in 5 minutes

4. Swap the code for tokens

cURL
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"
token.mjs
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());
exchange_token.py
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())
token.swift
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:

JSON
{
  "access_token": "cca_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "activities:read profile:read",
  "refresh_token": "ccr_...",
  "refresh_token_expires_in": 5184000
}
Shell
export ACCESS_TOKEN="cca_..."

5. Make your first call

cURL
curl "$CORRCONNECT_BASE/v1/activities?limit=5" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
activities.mjs
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());
activities.py
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())
activities.swift
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:

JSON
{
  "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.created as soon as a workout lands.
  • Scopes and grants explains what the athlete approved and how they take it back.