Guides
Read activities
Two endpoints cover everything: a newest-first list with an opaque cursor, and a single-record read for the IDs your webhooks give you.
List the newest activities
GET / returns up to limit records (default 25, 1 to 100), newest first by when CorrConnect stored them, plus a next_cursor. When next_cursor is null, you have everything.
The loop below walks every page. Each page costs one request, so 100 per page is the cheapest way to backfill.
# First page
curl "$CORRCONNECT_BASE/v1/activities?limit=100" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# Next page: pass next_cursor back exactly as you got it
curl -G "$CORRCONNECT_BASE/v1/activities" \
--data-urlencode "limit=100" \
--data-urlencode "cursor=$NEXT_CURSOR" \
-H "Authorization: Bearer $ACCESS_TOKEN"import { realpathSync } from "node:fs";
import { pathToFileURL } from "node:url";
export async function* allActivities(accessToken) {
let cursor = null;
do {
const url = new URL("/v1/activities", process.env.CORRCONNECT_BASE);
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } });
if (!response.ok) throw new Error(`${response.status} ${(await response.json()).code}`);
const page = await response.json();
yield* page.data;
cursor = page.next_cursor;
} while (cursor);
}
if (import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
let count = 0;
for await (const activity of allActivities(process.env.ACCESS_TOKEN)) {
count += 1;
console.log(activity.id, activity.activity_type, activity.started_at);
}
console.log(`${count} activities`);
}import os
from collections.abc import Iterator
import requests
def all_activities(access_token: str) -> Iterator[dict]:
cursor = None
while True:
params = {"limit": 100}
if cursor:
params["cursor"] = cursor
response = requests.get(os.environ["CORRCONNECT_BASE"] + "/v1/activities", params=params,
headers={"Authorization": f"Bearer {access_token}"}, timeout=10)
response.raise_for_status()
page = response.json()
yield from page["data"]
cursor = page["next_cursor"]
if cursor is None:
return
if __name__ == "__main__":
activities = list(all_activities(os.environ["ACCESS_TOKEN"]))
for activity in activities:
print(activity["id"], activity["activity_type"], activity["started_at"])
print(len(activities), "activities")import Foundation
struct Activity: Decodable {
let id: String
let source: String
let activity_type: String
let started_at: String
let duration_seconds: Double
let distance_meters: Double?
let title: String?
let created_at: String
}
struct Page: Decodable {
let data: [Activity]
let next_cursor: String?
}
func allActivities(base: String, accessToken: String) async throws -> [Activity] {
var results: [Activity] = []
var cursor: String?
repeat {
var components = URLComponents(string: base + "/v1/activities")!
components.queryItems = [URLQueryItem(name: "limit", value: "100")]
if let cursor { components.queryItems!.append(URLQueryItem(name: "cursor", value: cursor)) }
var request = URLRequest(url: components.url!)
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw URLError(.badServerResponse) }
let page = try JSONDecoder().decode(Page.self, from: data)
results += page.data
cursor = page.next_cursor
} while cursor != nil
return results
}
let env = ProcessInfo.processInfo.environment
let activities = try await allActivities(base: env["CORRCONNECT_BASE"]!, accessToken: env["ACCESS_TOKEN"]!)
for activity in activities { print(activity.id, activity.activity_type, activity.started_at) }
print(activities.count, "activities")The Swift Activity decodes only the fields every source can have; add the optional ones you use as optionals. Decoding still succeeds when CorrConnect adds new fields.
Keep up without re-reading everything
Records are ordered by created_at, the time CorrConnect stored them, not by when the workout happened. A file uploaded today with last month's ride comes first. So to catch up after a backfill:
- Remember the
idvalues (or the newestcreated_at) you have. - Read from the first page until you reach an
idyou already have, then stop.
Better still, subscribe to activity.created and fetch each new ID as it arrives. See webhooks.
Fetch one activity
GET / returns one record in the same shape as list items. Use it with the activity_id from an activity.created event.
curl "$CORRCONNECT_BASE/v1/activities/$ACTIVITY_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"const response = await fetch(
`${process.env.CORRCONNECT_BASE}/v1/activities/${process.env.ACTIVITY_ID}`,
{ headers: { Authorization: `Bearer ${process.env.ACCESS_TOKEN}` } },
);
if (response.status === 404) {
console.log("Not this athlete's activity, or it no longer exists");
} else {
console.log(response.status, await response.json());
}import os
import requests
response = requests.get(
f"{os.environ['CORRCONNECT_BASE']}/v1/activities/{os.environ['ACTIVITY_ID']}",
headers={"Authorization": f"Bearer {os.environ['ACCESS_TOKEN']}"},
timeout=10,
)
if response.status_code == 404:
print("Not this athlete's activity, or it no longer exists")
else:
print(response.status_code, response.json())import Foundation
let env = ProcessInfo.processInfo.environment
var request = URLRequest(url: URL(string: env["CORRCONNECT_BASE"]! + "/v1/activities/" + env["ACTIVITY_ID"]!)!)
request.setValue("Bearer \(env["ACCESS_TOKEN"]!)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as! HTTPURLResponse).statusCode
print(status == 404 ? "Not this athlete's activity, or it no longer exists" : String(decoding: data, as: UTF8.self))An ID that belongs to another athlete answers exactly like one that doesn't exist: 404 activity_.
Filter by source
Add source to get one kind of record, for example ?source=fit. Today's values are manual_json, gpx, tcx and fit; an unknown value fails with 400 invalid_. Leave it out to get everything.
Parse defensively
- Accept
source,activity_typeand basis values you don't know. Show them as "Other" and carry on. - Treat
distance_metersandtitleas optional and nullable. - Ignore fields you don't use. New ones will appear.
- Store
idand match on it; webhooks and retries can deliver the same ID twice.