Partner API
Integrate your application with Commit Swimming to access workout data on behalf of coaches and teams.
Overview
The Commit Swimming Partner API uses OAuth 2.0 Authorization Code flow. Your users authenticate with their Commit Swimming account, select which teams to share, and your app receives a Bearer token scoped to those teams.
To get started you need a client_id and client_secret issued by Commit Swimming.
Contact your Commit partnership representative to receive credentials.
https://utility.commitswimming.comUser Authorization URL:
https://team.commitswimming.com
OAuth Flow
-
Redirect the user to the Commit authorization page:
https://team.commitswimming.com/oauth/authorize ?client_id=YOUR_CLIENT_ID &redirect_uri=https://yourapp.com/callback &response_type=code &scope=workouts:read &state=RANDOM_STATE_STRINGThe user will sign in (if needed), see which teams your app is requesting access to, and approve or deny.
-
User approves. Commit redirects back to the
redirect_uriyour app specified:https://yourapp.com/callback?code=AUTHORIZATION_CODE&state=RANDOM_STATE_STRINGVerify the
statematches what you sent to prevent CSRF attacks. - Exchange the code for tokens by calling the token endpoint (see below).
Public clients (PKCE)
On-device and native apps that cannot keep a client_secret secret should use
PKCE (RFC 7636, the S256 method).
With PKCE you authorize and exchange the code entirely on the device—no client_secret and no backend required.
-
Generate a random code verifier (43–128 unreserved characters) and derive its
code challenge:
BASE64URL(SHA256(code_verifier)). -
Add
code_challengeandcode_challenge_method=S256to the authorize URL:https://team.commitswimming.com/oauth/authorize ?client_id=YOUR_CLIENT_ID &redirect_uri=https://yourapp.com/callback &response_type=code &scope=workouts:read &state=RANDOM_STATE_STRING &code_challenge=BASE64URL_SHA256_OF_VERIFIER &code_challenge_method=S256 -
At the token exchange, send the original
code_verifierinstead ofclient_secret. The server re-derives the challenge and rejects the request if it doesn't match.
Tokens issued via PKCE are public: refresh and revoke them with just your client_id (no secret).
Confidential clients that keep a client_secret on a server can keep using the secret-based flow below unchanged.
Token Exchange
POST/oauth/token
Exchange an authorization code for an access token and refresh token.
Request body (application/json)
| Parameter | Type | Description |
|---|---|---|
grant_type | string | "authorization_code" |
code | string | The authorization code from the redirect |
client_id | string | Your client ID |
client_secret | string | Your client secret. Confidential clients only—omit it when using PKCE. |
code_verifier | string | PKCE only. The original verifier whose challenge you sent to /oauth/authorize. Required (and replaces client_secret) for public clients. |
redirect_uri | string | The same redirect URI used in the authorize step |
Send either a client_secret (confidential clients) or a code_verifier (public/PKCE clients).
Example request
curl -X POST https://utility.commitswimming.com/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "abc123...",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"redirect_uri": "https://yourapp.com/callback"
}'
Response
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBh...",
"scope": "workouts:read",
"super_team_ids": ["teamId1", "teamId2"]
}
access_token expires in 1 hour.
refresh_token expires in 90 days.
Store both securely on your server—never expose them client-side.
Refreshing Tokens
POST/oauth/token
Use your refresh token to get a new access token before or after it expires.
Request body
{
"grant_type": "refresh_token",
"refresh_token": "dGhpcyBpcyBh...",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}
The response is the same shape as the initial token exchange. The old token pair is automatically revoked.
Public (PKCE) clients: omit client_secret—send only grant_type, refresh_token, and client_id.
Revoking Tokens
POST/oauth/revoke
Revoke an access token or refresh token when the user disconnects your app.
Request body
{
"token": "eyJhbGciOi...",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}
Always returns 200 OK regardless of whether the token was found (per RFC 7009).
Public (PKCE) clients: omit client_secret—send only token and client_id.
GET /api/v2/partner/me
GET/api/v2/partner/me
Returns information about the authenticated user and which teams were authorized.
Headers
Authorization: Bearer YOUR_ACCESS_TOKEN
Response
{
"userId": "abc123",
"firstName": "Jane",
"lastName": "Smith",
"email": "jane@example.com",
"superTeams": [
{ "_id": "team1", "name": "Metro Marlins" },
{ "_id": "team2", "name": "City Sharks" }
],
"scope": "workouts:read"
}
GET /api/v2/partner/workouts
GET/api/v2/partner/workouts
Fetch workouts within a date range for the authorized teams. Returns paginated results with structured set data for pace clock integration.
Headers
Authorization: Bearer YOUR_ACCESS_TOKEN
Query parameters
| Parameter | Required | Description |
|---|---|---|
startDate | Yes | Unix timestamp (seconds). Start of date range. |
endDate | Yes | Unix timestamp (seconds). End of date range. Max 365 days from startDate. |
superTeamIds | No | Comma-separated team IDs to filter. Must be a subset of authorized teams. Defaults to all authorized teams. |
skip | No | Number of results to skip (default: 0). |
limit | No | Results per page, 1–50 (default: 20). |
Example request
curl "https://utility.commitswimming.com/api/v2/partner/workouts?startDate=1710201600&endDate=1710460800&limit=10" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response
{
"workouts": [
{
"_id": "wkt_abc123",
"name": "AM Practice",
"text": "3 rounds\n 2 x 50 Free @ 1:00\n 2 rounds\n 4 x 100 Back @ 2:00",
"dateTime": "2026-03-12T06:00:00.000Z",
"endDateTime": null,
"course": { "distance": 25, "metric": "Yards" },
"trainingGroup": {
"_id": "tg_1",
"name": "Senior",
"superTeamId": "team1"
},
"groups": [
{ "_id": "grp_1", "name": "Distance" },
{ "_id": "grp_2", "name": "Sprint" }
],
"sets": [
{
"description": "3 rounds",
"rounds": 3,
"distance": null,
"interval": null,
"intervalCycle": null,
"intervalIsDefault": false,
"rest": null,
"stroke": null,
"strokeIsDefault": false,
"type": null,
"typeIsDefault": false,
"intensity": null,
"equipment": [],
"innerSets": [
{
"description": "2 x 50 Free @ 1:00",
"rounds": 2,
"distance": 50,
"interval": 60,
"intervalCycle": null,
"intervalIsDefault": false,
"rest": null,
"stroke": "Free",
"strokeIsDefault": false,
"type": "Swim",
"typeIsDefault": true,
"intensity": null,
"equipment": [],
"innerSets": []
},
{
"description": "2 rounds",
"rounds": 2,
"distance": null,
"interval": null,
"intervalCycle": null,
"intervalIsDefault": false,
"rest": null,
"stroke": null,
"strokeIsDefault": false,
"type": null,
"typeIsDefault": false,
"intensity": null,
"equipment": [],
"innerSets": [
{
"description": "4 x 100 Back @ 2:00",
"rounds": 4,
"distance": 100,
"interval": 120,
"intervalCycle": null,
"intervalIsDefault": false,
"rest": null,
"stroke": "Back",
"strokeIsDefault": false,
"type": "Swim",
"typeIsDefault": true,
"intensity": null,
"equipment": [],
"innerSets": []
}
]
}
]
}
],
"createdAt": "2026-03-11T22:00:00.000Z",
"updatedAt": "2026-03-11T23:15:00.000Z"
}
],
"pagination": {
"total": 42,
"skip": 0,
"limit": 10,
"hasMore": true
}
}
Scopes
Request only the scopes your application needs. The user will see what you are requesting on the consent screen.
| Scope | Description |
|---|---|
| workouts:read | Read workout data (text, structured sets, groups, course info) |
Additional scopes will be added as the API expands.
Structured Sets
Each workout includes a sets array—a recursive tree representing the workout structure.
This is ideal for pace clock integration, where you need to know rounds, distances, and intervals.
Set object
| Field | Type | Description |
|---|---|---|
description | string | The original text of this set line |
rounds | number | Number of rounds/reps (default: 1) |
distance | number | null | Distance per rep in yards/meters. null for parent sets. |
interval | number | null | Interval in seconds (e.g., 90 for 1:30). On a leaf set this is never null—if the coach didn't write one, the default is returned (see below). |
intervalCycle | array | null | Present only for alternating intervals like @ 1:30/2:00. The repeating pattern in seconds, e.g. [90, 120] (rep 1 on 1:30, rep 2 on 2:00, repeating). interval holds the first value. null when there is a single interval. |
intervalIsDefault | boolean | true when interval was filled with the default pace rather than written by the coach. |
rest | number | null | Rest in seconds between reps |
stroke | string | null | Stroke name: Free, Back, Fly, Breast, IM, Choice. Defaults to Free on a leaf set when unspecified. |
strokeIsDefault | boolean | true when stroke was defaulted rather than written by the coach. |
type | string | null | Activity type: Swim, Kick, Pull, Drill. Defaults to Swim on a leaf set when unspecified. |
typeIsDefault | boolean | true when type was defaulted rather than written by the coach. |
intensity | string | null | Intensity: Easy, Moderate, Fast, Sprint, Build, etc. Has no default—stays null when unspecified. |
equipment | array | Equipment names (currently always empty—coming soon) |
innerSets | array | Nested child sets. Empty array for leaf sets. |
Reading the tree
A set with innerSets is a parent set—its rounds tells you how many times to repeat
the children. A set with an empty innerSets and a distance is a leaf set—the actual swim piece.
// Pseudocode for a pace clock
function executeSet(set) {
for (let round = 0; round < set.rounds; round++) {
if (set.innerSets.length > 0) {
// Parent set: repeat children
for (const child of set.innerSets) {
executeSet(child)
}
} else {
// Leaf set: run the swim piece
startTimer(set.interval || set.rest)
display(set.distance, set.stroke)
}
}
}
Default values
A leaf set (one with a distance and no innerSets) always comes back ready to program a clock—the
same values Commit uses to compute totals are filled in when the coach left them out:
interval→distance × 1.2seconds (a 1:00/100 base pace), withintervalIsDefault: truestroke→"Free", withstrokeIsDefault: truetype→"Swim", withtypeIsDefault: true
The matching *IsDefault flag lets you tell a coach-written value from a filled-in one. intensity has no
default and stays null. Parent/header sets (no distance) are never defaulted. The default interval uses
the app-wide 1:00/100 base pace, which may differ from a team's custom pace setting.
Alternating intervals
When a coach writes two or more intervals separated by / — e.g. 10 x 100 @ 1:30/2:00 — the odd reps
go on the first and the even reps on the second. The response keeps interval as the first value and adds an
intervalCycle array with the repeating pattern:
{
"description": "10 x 100 @ 1:30/2:00",
"rounds": 10,
"distance": 100,
"interval": 90,
"intervalCycle": [90, 120],
...
}
Map rep n to intervalCycle[n % intervalCycle.length]. For a single interval, intervalCycle is null.
Errors
All errors return a JSON object with an error code and an error_description.
| HTTP Status | Error Code | Meaning |
|---|---|---|
| 400 | invalid_request | Missing or invalid parameters (e.g. a PKCE authorization with no code_verifier) |
| 400 | invalid_grant | Expired or already-used authorization code / refresh token, or failed PKCE verification |
| 400 | unsupported_grant_type | Use authorization_code or refresh_token |
| 401 | invalid_client | Bad client_id or client_secret |
| 401 | unauthorized | Missing or expired Bearer token |
| 403 | insufficient_scope | Token doesn't have the required scope |
| 403 | forbidden | Requested a superTeamId not authorized for this token |
| 405 | method_not_allowed | Wrong HTTP method |
Example error
{
"error": "invalid_request",
"error_description": "startDate and endDate are required (unix timestamps in seconds)"
}
Rate Limits
API requests are rate limited per access token. If you exceed the limit you will receive a
429 Too Many Requests response. Current limits:
| Window | Limit |
|---|---|
| Per second | 10 requests |
| Per hour | 500 requests |