Guides
Pagination and retries
Most CorrConnect calls are reads, and reads are safe to repeat. The few writes that aren't are exactly the ones you should never retry automatically.
Cursors
List endpoints return next_cursor. Pass it back as cursor with the same limit and source to get the next page; null means there are no more.
A cursor is signed and points at a position in the athlete's records (the last created_at and id you saw). That means:
- Don't build, edit or decode one. A cursor that fails the signature check returns
400 invalid_; start again without it.cursor - Cursors don't expire, and a page never repeats a record from an earlier page.
- Records stored after you started a walk appear on a fresh first page, not later in your walk. Start from the top to catch them, or use webhooks.
The webhook delivery log takes a limit (1 to 100, default 50) and has no cursor.
Which requests are safe to retry
| Request | Retry? |
|---|---|
GET anything | Yes, with backoff. |
POST /, POST / | Yes. Repeats answer {"status": "revoked"}. |
POST / with a code | No. A code works once, and a second use withdraws the tokens from the first. Start a new authorization. |
POST / with a refresh token | Only within 10 seconds, and only if you'll keep the second response. After that a repeat ends the grant. |
| Console changes (create an app or subscription) | Check first. Repeating a create makes a second app or subscription. |
CorrConnect has no Idempotency-Key header. Creates are rare and done by you, so list before you retry one.
Backing off
On 429, wait the number of seconds in Retry-After, then try again. details.scope tells you whether your whole app (app) or one athlete within it (athlete) ran out; for service_ the ceiling is shared by all apps and doesn't count against yours.
On a network error, a timeout or a 5xx, retry reads with exponential backoff and jitter, for example 1, 2, 4, 8 and 16 seconds plus a random fraction, and give up after five tries.
import { realpathSync } from "node:fs";
import { pathToFileURL } from "node:url";
export async function withBackoff(request, { tries = 5 } = {}) {
for (let attempt = 0; ; attempt += 1) {
let response;
try {
response = await request();
} catch (error) {
if (attempt + 1 >= tries) throw error; // network error
}
if (response && response.status !== 429 && response.status < 500) return response;
if (attempt + 1 >= tries) return response;
const retryAfter = Number(response?.headers.get("retry-after"));
const wait = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: 2 ** attempt * 1000 + Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, wait));
}
}
if (import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
const response = await withBackoff(() =>
fetch(`${process.env.CORRCONNECT_BASE}/v1/profile`, {
headers: { Authorization: `Bearer ${process.env.ACCESS_TOKEN}` },
}),
);
console.log(response.status, response.headers.get("ratelimit-remaining"));
}import os
import random
import time
import requests
def with_backoff(send, tries: int = 5) -> requests.Response:
for attempt in range(tries):
try:
response = send()
except requests.ConnectionError:
if attempt + 1 == tries:
raise
response = None
if response is not None and response.status_code != 429 and response.status_code < 500:
return response
if attempt + 1 == tries:
return response
retry_after = response.headers.get("Retry-After") if response is not None else None
time.sleep(int(retry_after) if retry_after else 2 ** attempt + random.random() / 4)
raise AssertionError("unreachable")
if __name__ == "__main__":
profile = with_backoff(lambda: requests.get(
os.environ["CORRCONNECT_BASE"] + "/v1/profile", timeout=10,
headers={"Authorization": f"Bearer {os.environ['ACCESS_TOKEN']}"}))
print(profile.status_code, profile.headers.get("RateLimit-Remaining"))Never put the token exchange inside a retry helper like this one.
Request IDs
Every response has an X-Request-ID header, and every error body repeats it as request_id. Log it with your own request; it's the fastest way for us to find what happened.