mako

Partner API

Read-only HTTPS access to your organization's MakoSwim data: workouts as written and as swum, races with splits and stroke rates, attendance, test sets and wellness. This page documents v1: authentication, conventions, the twelve endpoints, and the OpenAPI contract.

mako partner api
#

Overview

The API is organized around twelve list resources under /v1. Every endpoint returns rows in the same JSON envelope, authenticates with the same bearer key, and uses standard HTTP response codes. GET is the only method. No SDK is required.

base url
https://api.makoswim.io/v1

Workouts are exposed in three layers: the text exactly as the coach wrote it (workout-content), the parsed structure of every line (structured=true), and the per-athlete volumes the training engine computed when the session was saved (workouts, workout-segments, workout-set-volumes). Attribution is joint: a segment row carries zone, stroke, style and equipment together, so "fly kick at zone 2 in July, per athlete" is one filter and one group-by on workout-segments.

#

Getting access

The Data API ships with the MakoSwim federation tier. Your federation authorizes access in writing, and we issue an organization-scoped key with exactly the scopes you request. Keys are shown once at issuance; treat them like passwords.

A key carries only the scopes that were requested:

ScopeUnlocks
rosterAthletes and teams
workoutsWorkout aggregates, segments, content, set volumes, terminology
attendanceSession attendance
performancesRace results with splits, stroke counts and rates
metricsTest-set definitions and results
wellnessDaily wellness surveys. Health data: never granted by default, only on explicit written instruction from your federation

To request access or add a scope, write to eric@makoapp.io from your federation account. To roll a key, email us: we issue the replacement first, and revoke the old one after you switch.

#

Authentication

Send the key as a bearer token on every request. Keys look like mk_live_ followed by 43 to 60 characters, and identify your organization on our side: the API never takes an org identifier from the caller, so a key can only ever read the organization it was issued for.

A missing, malformed, unknown or revoked key returns 401 unauthorized. A valid key whose organization is not on the federation tier returns 403 forbidden_federation. A valid key used outside its scopes returns 403 forbidden_scope and names the missing scope.

request
curl -H "Authorization: Bearer mk_live_YOUR_KEY" \
  "$BASE/v1/athletes?limit=50"
401 · problem+json
{
  "code": "unauthorized",
  "title": "Unauthorized",
  "detail": "Provide an API key: Authorization: Bearer mk_live_…",
  "status": 401
}
#

Envelope & pagination

Every list response is the same shape: data holds the rows, has_more tells you whether to keep going, and next_cursor is an opaque token you pass back as cursor. Walk until has_more is false. On resources that explode source documents into rows (workouts, segments, set volumes, attendance), a page can hold more rows than limit (bounded near 10,000, whole documents only) or fewer, including an empty page with has_more: true. Terminate the walk on has_more, never on an empty data array.

Cursors are opaque on purpose. Do not parse or construct them; a tampered cursor returns 400 invalid_cursor. Page size is limit, from 1 to 500, defaulting to 100. Every endpoint accepts limit and cursor; the endpoint sections list only resource-specific parameters.

Attributes
dataarray
The rows.
has_moreboolean
Whether more rows exist beyond this page.
next_cursornullable string
Opaque cursor for the next page; null on the last page.
every response
{
  "data": [ …rows… ],
  "has_more": true,
  "next_cursor": "eyJkIjoiMjAyNi0wOC0xMiIsImlkIjoi…"
}
next page
curl -H "Authorization: Bearer $KEY" \
  "$BASE/v1/workouts?cursor=eyJkIjoiMjAyNi0wOC0xMiIsImlkIjoi…"
#

Filters & dates

Where a resource has dates, from and to are inclusive YYYY-MM-DD bounds. Most resources also filter by teamId and athleteId. Training dates are calendar dates, deliberately without timezone; timestamps, where present, are RFC 3339 UTC. Dates must be real calendar dates with from on or before to, and unknown query parameters are rejected with 400 unknown_param: a typoed filter never returns silently unfiltered data.

GET and HEAD are the only methods (HEAD returns the same headers without a body). There is no CORS support on purpose: API keys must never live in a browser.

Responses are gzip-compressed when the client sends Accept-Encoding: gzip, and carry Cache-Control: no-store because rows contain personal data.

#

Rate limits

Per key: a burst of 20 requests, refilling at 10 per second, inside a cap of 3,000 per hour. Every response to a valid key carries the headers below; a 429 adds Retry-After. Back off with jitter. Requests are served up to four at a time: pull sequentially, or keep parallel pulls at four or fewer. At 500 rows per page, the hourly cap covers 1.5 million rows.

response headers
X-RateLimit-Limit: 3000
X-RateLimit-Remaining: 2984
X-RateLimit-Reset: 2711
Retry-After: 1        # on 429 only
#

Errors

Errors are RFC 9457 application/problem+json, with a stable machine-readable code. Branch on the code; log the detail for debugging.

The error object
typestring
URI identifying the error class (https://api.makoswim.io/problems/<code>).
titlestring
Short summary of the error class.
statusinteger
HTTP status code, repeated in the body.
codeenum
Stable machine-readable code; the full list is below.
detailstring
Human-readable explanation of this occurrence.
paramstring
The query parameter at fault; present on invalid_param and invalid_cursor.
StatusCodeMeaning
400invalid_paramA query parameter failed validation. The response names it.
400unknown_paramThe parameter is not one this endpoint accepts. The response names it and lists the accepted set.
400invalid_cursorThe cursor is not one we issued. Restart the walk.
401unauthorizedMissing, malformed, unknown or revoked key.
403forbidden_federationThe organization is not on the federation tier.
403forbidden_scopeThe key does not carry the required scope.
404not_foundUnknown endpoint. The response lists the known ones.
405method_not_allowedThe API is read-only. Use GET.
429rate_limitedRate limit exceeded. Honor Retry-After before retrying.
500internalUnexpected server error, logged on our side with the request.

#

GET/v1/athletesscope roster

One row per athlete: name, birth date, main stroke, and team and training-group membership. Other resources reference people by athleteId and join here. Contact fields are never exposed.

Attributes
idstring
Unique identifier for the athlete within the organization.
firstNamenullable string
The athlete's first name.
lastNamenullable string
The athlete's last name.
gendernullable enum
M, F, or null when not recorded.
dateOfBirthnullable date
Date of birth (YYYY-MM-DD).
mainStrokenullable string
The athlete's main stroke, as set on the roster.
teamIdnullable string
Identifier of the team the athlete belongs to.
trainingGroupIdnullable string
Identifier of the athlete's training group within the team.
statusnullable string
Roster status, as shown in the app (for example active).
hasAccountboolean
Whether the athlete has a linked app account.
Query parameters
teamIdLimit to one team
request
curl -H "Authorization: Bearer $KEY" \
  "$BASE/v1/athletes?teamId=t8xK2p&limit=100"
row
{
  "id": "aQ71Lm",
  "firstName": "Lena",
  "lastName": "Fischer",
  "gender": "F",
  "dateOfBirth": "2008-03-21",
  "mainStroke": "fly",
  "teamId": "t8xK2p",
  "trainingGroupId": "g_sprint",
  "status": "active",
  "hasAccount": true
}
#

GET/v1/teamsscope roster

Teams with their site and training groups. In a federation setup a site is typically a training base and teams are the squads inside it. Training-group ids from here appear on athlete rows and workout rosters.

Attributes
idstring
Unique identifier for the team.
namenullable string
Team name.
descriptionnullable string
Team description.
siteIdnullable string
Identifier of the site (training base) the team belongs to.
siteNamenullable string
Name of that site.
trainingGroupsarray of objects
The team's training groups.
idstring
Training-group identifier, referenced by athletes and workout rosters.
namenullable string
Group name.
colornullable string
Display color (hex).
row
{
  "id": "t8xK2p",
  "name": "National Training Base Berlin",
  "siteId": "s_berlin",
  "siteName": "Berlin",
  "trainingGroups": [
    { "id": "g_sprint", "name": "Sprint", "color": "#596AF7" }
  ]
}
#

GET/v1/workoutsscope workouts

One row per athlete per workout: total volume, training load, and marginal breakdowns by energy zone, stroke, training style and equipment, as computed by the engine when the session was saved.

Attributes
workoutIdstring
Identifier of the workout this row aggregates.
datenullable date
Session date (YYYY-MM-DD).
sessionnullable string
Session: am or pm, always lower case.
sessionIndexnullable integer
Ordinal of the session within the day.
teamIdnullable string
Identifier of the team.
poolTypenullable enum
SCY, SCM, or LCM.
athleteIdstring
Identifier of the athlete.
totalVolumenumber
Total distance swum by this athlete, in the pool's units.
trainingLoadnullable number
Training load computed for this athlete for the session.
byEnergymap
Distance per energy-zone id.
byStrokemap
Distance per stroke id.
byTrainingStylemap
Distance per training-style id.
byEquipmentmap
Distance per equipment id.
estimatedDurationSecondsnullable number
Estimated session duration for this athlete, in seconds.
Query parameters
from, toInclusive date bounds
teamIdOne team
athleteIdOne athlete
row
{
  "workoutId": "w_2026-08-12_am",
  "date": "2026-08-12",
  "session": "am",
  "poolType": "LCM",
  "athleteId": "aQ71Lm",
  "totalVolume": 5400,
  "trainingLoad": 312,
  "byEnergy": { "BZ1": 2600, "BZ2": 2000, "BZ5": 800 },
  "byStroke": { "free": 4200, "fly": 1200 },
  "byTrainingStyle": { "swim": 4600, "kick": 800 },
  "estimatedDurationSeconds": 6930
}
#

GET/v1/workout-segmentsscope workouts

The attribution behind those marginals, in long format: one row per athlete, workout and segment, carrying volume, zone, stroke, style and equipment together. Marginals cannot answer cross-dimension questions (800m of kick and 800m at BZ2 may or may not be the same 800); segment rows can.

medley=resolved (default) distributes IM across its strokes; medley=preserved keeps it as im.

Attributes
workoutIdstring
Identifier of the workout the segment belongs to.
datenullable date
Session date (YYYY-MM-DD).
sessionnullable string
Session: am or pm, always lower case.
teamIdnullable string
Identifier of the team.
athleteIdstring
Identifier of the athlete.
volumenumber
Distance of this segment, in the pool's units.
zonenullable string
Energy-zone id; resolves via /v1/terminology. null when the stored segment names no single zone.
strokenullable string
Stroke id. With medley=resolved, IM is distributed across its strokes. null when the stored segment names no single stroke.
stylenullable string
Training-style id (for example swim, kick, pull).
equipmentarray of strings
Equipment ids attached to the segment.
Query parameters
from, toInclusive date bounds (YYYY-MM-DD)
teamIdLimit to one team
athleteIdLimit to one athlete
medleyresolved (default) or preserved
fly kick at bz2, per athlete
curl -H "Authorization: Bearer $KEY" \
  "$BASE/v1/workout-segments?from=2026-07-01&to=2026-07-31"
row
{
  "workoutId": "w_2026-08-12_am",
  "date": "2026-08-12",
  "athleteId": "aQ71Lm",
  "volume": 400,
  "zone": "BZ2",
  "stroke": "fly",
  "style": "kick",
  "equipment": ["fins"]
}
#

GET/v1/workout-contentscope workouts

The prescription itself, one row per workout: every set exactly as the coach wrote it, with rounds, set type, and which groups or athletes each set targets. Set text is byte-verbatim, including leading whitespace: indentation encodes nesting depth and line breaks delimit lines. Never normalize it.

The row also carries the resolved roster state (group reassignments and stroke overrides, keyed by athleteId). Coach post-workout reflections are private and are not exposed.

With structured=true, every set also carries a parsed, line-by-line structure; see the next section.

Attributes
workoutIdstring
Unique identifier for the workout.
datenullable date
Session date (YYYY-MM-DD).
sessionnullable string
Session: am or pm, always lower case.
sessionIndexnullable integer
Ordinal of the session within the day.
teamIdnullable string
Identifier of the team.
poolTypenullable string
Pool type the workout was written for.
namenullable string
Workout name, as titled by the coach.
workoutTypenullable string
Identifier of the workout type; resolves via /v1/terminology.
workoutTypeNamenullable string
Display name of the workout type.
isLiveboolean
Whether the workout is currently running in live mode.
seasonCycleIdnullable string
Identifier of the season cycle the workout belongs to.
weekNumbernullable integer
Week number within that cycle.
trainingGroupsarray of strings
Training-group ids the workout targets.
setsarray of objects
The sets, in order.
idnullable string
Set identifier; joins to /v1/workout-set-volumes.
ordernullable integer
Position of the set within the workout.
typenullable string
Set type (group, individual, or dryland).
displayNamenullable string
The set's title as the coach named it.
roundsinteger
Number of rounds. Lines are not multiplied out.
groupsarray of strings
Training-group ids this set targets.
athletesarray of strings
Athlete ids this set targets directly.
replacesnullable object
On an individual set that stands in place of other sets: the athletes in forAthletes swim this set and not the sets in setIds. null when the set is extra work.
setIdsarray of strings
Ids of the sets it stands in for.
forAthletesarray of strings
Athlete ids it applies to.
contentstring
The set text, byte-verbatim.
structurednullable array
Parsed lines. Present only with structured=true; null when the team has no terminology config.
swimmerVersionsnullable object
Present when the coach wrote lines of the set for named swimmers. content holds every line; this gives the set as each swimmer swims it. null when the set names nobody.
everyoneElseobject
The set as every swimmer who is not named swims it.
contentstring
Set text.
structurednullable array
Parsed lines. Present only with structured=true.
versionsarray of objects
One entry per different text. Swimmers who swim the same text share an entry.
athleteIdsarray of strings
Athletes who swim this text.
contentstring
The set as these athletes swim it.
structurednullable array
Parsed lines. Present only with structured=true.
messagesarray of objects
Coach messages shown to the athletes with the workout.
idnullable string
Message identifier.
ordernullable integer
Position of the message.
textnullable string
Message text.
rosterobject
Resolved roster state.
useExplicitRosterboolean
Whether the workout uses an explicit athlete list instead of group membership.
assignedAthleteIdsarray of strings
Athlete ids explicitly assigned.
guestAthleteIdsarray of strings
Athlete ids attending as guests from other teams.
flexRostermap
Per-athlete group reassignment for this workout: athleteId to { assignedGroup, originalGroup }.
strokeFlexAssignmentsmap
Per-set stroke overrides: setId to athleteId to { strokeOverride }.
Query parameters
from, toInclusive date bounds
teamIdOne team
structuredtrue to parse each set with the production engine
row (trimmed)
{
  "workoutId": "w_2026-08-12_am",
  "name": "Main aerobic block",
  "workoutTypeName": "Endurance",
  "sets": [{
    "id": "set_1",
    "type": "main",
    "rounds": 3,
    "groups": ["g_sprint"],
    "content": "8 x 50 fly BZ2 @0:50\n  4 x 25 kick\n> 200 easy"
  }],
  "roster": {
    "flexRoster": { "aQ71Lm": { "assignedGroup": "g_dist", "originalGroup": "g_sprint" } }
  }
}
#

Structured parsing

With structured=true, each set's text is parsed by the same engine the app itself uses, against your organization's own terminology. Every line comes back with its engine-computed volume, its attribution segments, intervals, skill counts and flags for post-round lines and nesting depth.

The accuracy contract: every field is either produced by the engine or proven against it before it ships; anything unprovable is null. reps and distance ship only when their product equals the engine's volume, so a sum line like 500 + 5x100 + 500 keeps its volume and segments while both scalars stay null. segments is always the authoritative decomposition.

The line object
textstring
The line, byte-verbatim, including leading whitespace.
indentinteger
Leading-whitespace length; encodes nesting depth.
volumenumber
Engine-computed distance for the line, in the pool's units. 0 for skill lines.
repsnullable integer
Repetition count. Emitted only when reps times distance equals volume; null otherwise.
distancenullable number
Distance per repetition. Same emission rule as reps.
intervalsarray of objects
Every interval on the line.
patternstring
The interval token as written (for example @0:50).
secondsnullable number
Parsed seconds.
intervalSecondsnullable number
Parsed seconds when the line has exactly one interval; null otherwise.
segmentsarray of objects
The authoritative attribution decomposition.
volumenumber
Segment distance.
zonenullable string
Energy-zone id.
strokenullable string
Stroke id.
stylenullable string
Training-style id.
equipmentarray of strings
Equipment ids.
skillCountsarray of objects
Counts for skill lines.
skillIdnullable string
Skill id; resolves via /v1/terminology.
termnullable string
Skill term as matched.
countnullable number
Repetitions.
restnullable string
Rest token, as written.
distancePerRepnumber
Distance per rep. Present when the skill has one.
distancePerRepUnitenum
meters or yards. Present with distancePerRep.
linkedEnergyZonestring
Zone the skill distance is credited to: a zone word on the line, else the skill's own zone.
linkedStrokestring
Stroke key credited. Present when the skill has a stroke, from the line or the skill's default.
linkedStylenullable string
Training-style key credited. Present with linkedStroke.
linkedEquipmentarray of strings
Equipment keys credited. Present with linkedStroke and at least one item.
lineWordsobject
The stroke, type, zone and gear words the line itself wrote. Present only when it wrote any.
strokenullable string
Stroke word, as written.
stylenullable string
Type word, as written.
zonenullable string
Zone word, as written.
equipmentarray of strings
Gear words, as written.
skillVolumenullable number
Skill meters (count times distancePerRep). Separate from volume because the app does not count skill work as swim volume.
notesnullable string
Trailing note text, when the line carries one.
flagsobject
Line flags.
postRoundboolean
The line executes once after all rounds.
skillLineboolean
The line is a skill line.
restSecondsnullable number
Seconds for timed-break lines.
headerboolean
Indent Mode only. The line is a block label with volume 0; its words apply to the lines under it.
headerMultipliernullable number
Indent Mode only, on a header. How many times its block is swum.
inheritedFromarray of strings
Indent Mode only. The header lines this line took its stroke, type, zone or gear from.
"500 + 5x100 + 500" · parsed
{
  "text": "500 + 5x100 + 500",
  "volume": 1500,
  "reps": null,
  "distance": null,
  "segments": [
    { "volume": 500, "zone": "BZ1", "stroke": "free" },
    { "volume": 500, "zone": "BZ1", "stroke": "free" },
    { "volume": 500, "zone": "BZ1", "stroke": "free" }
  ],
  "flags": { "postRound": false, "skillLine": false }
}
"8 x 50 fly BZ2 @0:50" · parsed
{
  "volume": 400,
  "reps": 8,
  "distance": 50,
  "intervalSeconds": 50,
  "segments": [{ "volume": 400, "zone": "BZ2", "stroke": "fly", "style": "swim" }]
}
#

GET/v1/workout-set-volumesscope workouts

One row per athlete, workout and set: total volume, training load and breakdowns for that set alone. setId joins to the sets in /v1/workout-content, so you can compare the prescribed set with what each athlete swam.

Attributes
workoutIdstring
Identifier of the workout.
setIdstring
Set identifier; joins to sets in /v1/workout-content.
datenullable date
Session date (YYYY-MM-DD).
sessionnullable string
Session: am or pm, always lower case.
teamIdnullable string
Identifier of the team.
athleteIdstring
Identifier of the athlete.
totalVolumenumber
Distance swum by this athlete in this set, in the pool's units.
trainingLoadnullable number
Training load for this set alone.
byEnergymap
Distance per energy-zone id, for this set.
byStrokemap
Distance per stroke id, for this set.
byTrainingStylemap
Distance per training-style id, for this set.
byEquipmentmap
Distance per equipment id, for this set.
Query parameters
from, toInclusive date bounds (YYYY-MM-DD)
teamIdLimit to one team
athleteIdLimit to one athlete
row
{
  "workoutId": "w_2026-08-12_am",
  "setId": "set_1",
  "athleteId": "aQ71Lm",
  "totalVolume": 1800,
  "trainingLoad": 96,
  "byEnergy": { "BZ2": 1800 }
}
#

GET/v1/terminologyscope workouts

Your organization's terminology: energy zones with RPE ranges, strokes, training styles, skills and workout types, each with the aliases coaches actually write. Zone, stroke and style ids used in segment rows and parsed lines resolve here.

Attributes
idstring
Unique identifier for the terminology config.
namenullable string
Config name.
teamIdnullable string
Owning team, when team-scoped.
versionnullable string
Config schema version.
defaultEnergyZonenullable string
Energy-zone id applied when a line names none.
energySystemsarray of objects
Energy zones.
idnullable string
Zone id, as used in segment rows and parsed lines.
termnullable string
Short term (for example BZ2).
fullNamenullable string
Full name.
colornullable string
Display color (hex).
rpeMinnullable number
Lower bound of the RPE range.
rpeMaxnullable number
Upper bound of the RPE range.
aliasesarray of strings
Alternative spellings coaches write.
strokesarray of objects
Strokes, with the same shape as energy zones minus the RPE range.
idnullable string
Stroke id.
termnullable string
Short term.
fullNamenullable string
Full name.
colornullable string
Display color (hex).
aliasesarray of strings
Alternative spellings.
stylesarray of objects
Training styles and equipment.
idnullable string
Style id.
termnullable string
Short term.
fullNamenullable string
Full name.
colornullable string
Display color (hex).
aliasesarray of strings
Alternative spellings.
categorynullable string
training or equipment.
skillsarray of objects
Count-based skills (starts, turns, and similar).
idnullable string
Skill id.
termnullable string
Short term.
fullNamenullable string
Full name.
colornullable string
Display color (hex).
aliasesarray of strings
Alternative spellings.
linkedEnergyZonenullable string
Zone credited for skill work.
distancePerRepnullable number
Meters credited per repetition.
distancePerRepUnitnullable string
meters or yards.
linkedMetricIdnullable string
MakoBoards metric the skill is tracked with.
linkedStrokeIdnullable string
Default stroke, an id in strokes.
linkedStyleIdnullable string
Default training style, an id in styles.
linkedEquipmentIdsarray of strings
Default equipment, ids in styles.
workoutTypesarray of objects
Workout types.
idnullable string
Workout-type id.
termnullable string
Short term.
fullNamenullable string
Full name.
colornullable string
Display color (hex).
descriptionnullable string
Description.
row (trimmed)
{
  "name": "Federation terms",
  "energySystems": [{
    "id": "es_bz2",
    "term": "BZ2",
    "rpeMin": 3, "rpeMax": 5,
    "aliases": ["GA1", "aerobic"]
  }],
  "strokes": [ … ],
  "skills": [ … ]
}

#

GET/v1/attendancescope attendance

One row per athlete per session. status is one of present, late, absent, excused, planned_absence or made_up, with the training group at the time and the make-up date where relevant. made_up counts as present in the app's own summaries.

Attributes
attendanceIdstring
Identifier of the attendance record (one per team session).
teamIdnullable string
Identifier of the team.
datenullable date
Session date (YYYY-MM-DD).
sessionnullable string
Session: am or pm, always lower case.
sessionIndexnullable integer
Ordinal of the session within the day.
workoutIdnullable string
Workout linked to the session, when one exists.
athleteIdstring
Identifier of the athlete.
statusnullable enum
present, late, absent, excused, planned_absence, or made_up.
trainingGroupIdnullable string
The athlete's training group at the time.
notesnullable string
Coach note on the record. A free-text note can hold health information, so it is published only to a key that also carries the wellness scope, and never for an athlete excluded from wellness. null otherwise.
madeUpOnnullable date
Date the session was made up, for made_up records.
isGuestboolean
Whether the athlete attended as a guest from another team.
leftEarlyboolean
Whether the swimmer left before the end of the session.
adjustedVolumenullable object
What the app counts for a swimmer who swam only part of the session, in place of their row in /v1/workouts. null when the workout's own numbers stand.
totalVolumenumber
Meters counted for the swimmer.
trainingLoadnullable number
Training load counted for the swimmer.
byEnergymap
Distance per energy-zone id.
byStrokemap
Distance per stroke id.
byTrainingStylemap
Distance per training-style id.
byEquipmentmap
Distance per equipment id.
Query parameters
from, toInclusive date bounds (YYYY-MM-DD)
teamIdLimit to one team
athleteIdLimit to one athlete
row
{
  "date": "2026-08-12",
  "session": "am",
  "athleteId": "aQ71Lm",
  "status": "made_up",
  "madeUpOn": "2026-08-14",
  "workoutId": "w_2026-08-12_am"
}
#

GET/v1/performancesscope performances

Event swims with total time, reaction time and AquaPoints, plus race-analysis detail where it exists: cumulative taggedTimes at marked distances, with strokeCounts and strokeRates at measurement points. type separates competition swims from training swims, timed race-style efforts done at practice. A relay is one row, on its lead-off swimmer, with its record and legs in relay.

Attributes
idstring
Unique identifier for the swim.
athleteIdnullable string
Identifier of the athlete.
teamIdnullable string
Identifier of the team.
datenullable date
Swim date (YYYY-MM-DD).
typenullable enum
competition, or training for race-style efforts at practice.
eventnullable string
Event name (for example 100 Fly).
strokenullable string
Stroke id.
poolTypenullable string
Pool type of the swim.
totalTimeSecondsnullable number
Final time, in seconds.
reactionTimeSecondsnullable number
Reaction time, in seconds.
startTypenullable string
Start type recorded for the swim.
timingMethodnullable enum
How the zero of the race clock was set: light_sync (the start light seen on video), reaction_time (derived from the typed official reaction time) or electronic (the time comes from a results file).
suitTypenullable string
Suit recorded for the swim.
aquaPointsnullable number
AquaPoints score for the swim.
competitionIdnullable string
Identifier of the competition.
competitionNamenullable string
Competition name.
stagenullable string
heats, semi-finals or finals where the result says so.
isManualEntryboolean
Whether the swim was entered by hand rather than imported or tagged.
relaynullable object
Relay record. null for an individual swim. A relay comes from a results file and is stored once, on the swimmer the file names as lead-off: that swimmer is the row's athleteId and the relay's total is totalTimeSeconds. Who swam the other legs is not recorded.
typenullable string
Legs × leg distance: the event without its unit (for example 4x100).
stylenullable string
medley: backstroke, breaststroke, butterfly, freestyle in that order. free: every leg swims the row's stroke. null on a relay imported before the style was kept.
legsarray of objects
The legs the results file listed, in swimming order. Can hold fewer than the relay has, or none.
ordernullable integer
Position in the relay. 1 is the lead-off.
strokenullable string
Stroke of the leg.
legTimeSecondsnullable number
Time of the leg, when the results file gave one.
exchangeTimeSecondsnullable number
Takeover time of the leg, when the results file gave one. Negative for an early takeover. Always null on leg 1.
taggedTimesmap of numbers
Cumulative race time in seconds from the start signal at each marked distance, keyed by the distance (for example "50m": 27.93). The value at the event distance equals totalTimeSeconds. From the video marks of a tagged race or the results file of an imported one.
strokeCountsmap of integers
Strokes counted per length from video, keyed segment_N (N × 50 m in LCM, 25 m in SCM, 25 y in SCY). A length that was not counted has no key.
kickCountsmap of integers
Underwater kicks counted per length from video, keyed segment_N like strokeCounts. A length that was not counted has no key.
breathCountsmap of integers
Breaths counted per length from video, keyed segment_N like strokeCounts. A length that was not counted has no key.
strokeRatesmap of numbers
Stroke rate in strokes per minute at each measurement point, keyed by the point's distance from the start (for example "20m").
startAnalysisnullable object
Start-phase measurements taken from video. null when the race has none. The entry point is the swimmer's cap breaking the surface; distances are metres from the wall.
frontFootOffSecondsnullable number
Start signal to last contact leaving the start, measured from video. null when timingMethod is reaction_time.
handReleaseSecondsnullable number
Start signal to the hands letting go.
rearFootOffSecondsnullable number
Start signal to the rear foot leaving the block. Always null on a backstroke race.
firstMovementSecondsnullable number
Start signal to first visible movement, when marked.
movementTimeSecondsnullable number
First movement to last contact leaving the start.
flightTimeSecondsnullable number
Last contact leaving the start to cap entry.
entryDistanceMetersnullable number
Cap entry distance from the wall.
entryDistanceUncertaintyMetersnullable number
Resolution bound of the entry distance at the clip's frame rate.
entryToBreakoutSecondsnullable number
Cap entry to the tagged breakout.
videoFrameDurationSecondsnullable number
Measured duration of one video frame.
Query parameters
from, toInclusive date bounds (YYYY-MM-DD)
teamIdLimit to one team
athleteIdLimit to one athlete
typecompetition or training
row (trimmed)
{
  "athleteId": "aQ71Lm",
  "date": "2026-07-19",
  "type": "competition",
  "event": "100m",
  "stroke": "butterfly",
  "poolType": "LCM",
  "totalTimeSeconds": 59.84,
  "reactionTimeSeconds": 0.68,
  "aquaPoints": 812,
  "taggedTimes": { "15m": 6.91, "50m": 27.93 },
  "strokeRates": { "20m": 52.4 }
}
#

GET/v1/metricsscope metrics

The catalog of coach-defined test sets and measurements: time or count, unit, distance, pool type, and for composite tests the full rep structure. Read this once to interpret /v1/metric-results.

Attributes
idstring
Unique identifier for the metric definition.
namenullable string
Metric name.
descriptionnullable string
Metric description.
typenullable enum
time or count.
unitnullable string
Unit for count metrics.
distancenullable number
Distance of the test, when distance-based.
poolTypenullable string
Pool type the metric is defined for.
scopenullable enum
account (all teams) or team.
teamIdnullable string
Owning team for team-scoped metrics.
isActiveboolean
Whether the metric is currently in use.
isCompositeboolean
Whether the metric has a multi-rep structure.
compositeStructurenullable object
Rep-group structure for composite metrics: distances, reps, rest, stroke, and split configuration.
row
{
  "id": "m_7x200",
  "name": "7 x 200 step test",
  "type": "time",
  "poolType": "LCM",
  "isComposite": true,
  "compositeStructure": { "repGroups": [ … ] }
}
#

GET/v1/metric-resultsscope metrics

Recorded results against those definitions, including per-rep values and split times for composite tests. Filter by metricId to pull one test's full history across the squad.

Attributes
idstring
Unique identifier for the result.
athleteIdnullable string
Identifier of the athlete.
teamIdnullable string
Identifier of the team.
metricIdnullable string
Definition this result was recorded against.
metricNamenullable string
Definition name at the time of recording.
datenullable date
Recording date (YYYY-MM-DD).
sessionnullable string
Session: am or pm, always lower case, when recorded in a session.
workoutIdnullable string
Workout the result was recorded in, when applicable.
valuenullable number
Numeric value; seconds for time metrics.
displayValuenullable string
Formatted value as shown in the app.
typenullable string
Copied from the definition (time or count).
unitnullable string
Copied from the definition.
poolTypenullable string
Pool type at recording.
strokenullable string
Stroke, when recorded.
suitnullable string
Suit, when recorded.
notesnullable string
Note on the result.
isCompositeboolean
Whether the result carries per-rep detail.
compositeStructurenullable object
The rep-group structure the result was recorded against.
repValuesarray of nullable numbers
Per-rep values for composite results. null for a rep that was not entered.
repSplitTimesmap
Split times per rep, keyed by rep index, when recorded. null for a rep with none.
splitTimesarray of nullable numbers
Split times in seconds of a split-based single metric, one per splitDistance.
splitDistancenullable number
Distance of each split in splitTimes, in the pool's unit.
totalDistancenullable number
Total distance of the test.
createdAtnullable timestamp
When the result was recorded (RFC 3339 UTC).
Query parameters
from, toInclusive date bounds (YYYY-MM-DD)
teamIdLimit to one team
athleteIdLimit to one athlete
metricIdLimit to one metric definition
row (trimmed)
{
  "athleteId": "aQ71Lm",
  "metricId": "m_7x200",
  "date": "2026-08-05",
  "displayValue": "2:14.6 avg",
  "repValues": [139.2, 137.8, 136.1, 134.6, 133.0, 131.4, 128.9]
}
#

GET/v1/wellnessscope wellness

Daily athlete surveys: energy, sleep, soreness, stress and willingness to train, plus the composite wellnessScore and mood quadrant.

Rows are name-free: join the roster by athleteId only where your legal basis allows it. Athletes whose consent is withheld or withdrawn are absent from every response, including direct queries for their id.

Attributes
idstring
Identifier of the survey (one per athlete per day).
athleteIdnullable string
Identifier of the athlete. Rows carry no names.
teamIdnullable string
Identifier of the team.
datenullable date
Survey date (YYYY-MM-DD).
energynullable number
1 to 5.
sleepHoursnullable number
Hours slept.
sleepQualitynullable number
1 to 5.
muscleSorenessnullable number
1 to 5; lower is better.
lifeStressnullable number
1 to 5; lower is better.
willingnessToTrainnullable number
1 to 5.
wellnessScorenullable number
Composite score, 0 to 100.
moodQuadrantnullable string
Mood quadrant from the daily check-in.
submittedAtnullable timestamp
When the survey was submitted (RFC 3339 UTC).
Query parameters
from, toInclusive date bounds (YYYY-MM-DD)
teamIdLimit to one team
athleteIdLimit to one athlete
row
{
  "athleteId": "aQ71Lm",
  "date": "2026-08-12",
  "energy": 4,
  "sleepHours": 7.5,
  "sleepQuality": 4,
  "muscleSoreness": 2,
  "lifeStress": 1,
  "willingnessToTrain": 5,
  "wellnessScore": 87,
  "moodQuadrant": "green"
}

#

Pulling into pandas

Rows are long-format. The cursor walk is identical for every resource; the helper below works for all twelve endpoints.

python
import requests, pandas as pd

BASE = "https://api.makoswim.io/v1"
H = {"Authorization": "Bearer mk_live_YOUR_KEY"}

def pull(resource, **params):
    rows, cursor = [], None
    while True:
        r = requests.get(f"{BASE}/{resource}", headers=H,
                         params={**params, "cursor": cursor, "limit": 500}).json()
        rows += r["data"]
        if not r["has_more"]: return pd.DataFrame(rows)
        cursor = r["next_cursor"]

segments = pull("workout-segments", **{"from": "2026-07-01", "to": "2026-07-31"})
fly_kick = segments.query("stroke == 'fly' and style == 'kick'") \
                   .groupby("athleteId").volume.sum()
#

Data protection

This section summarizes the processing posture. The full annex, with technical and organizational measures, is part of your federation agreement.

ResidencyThe API and its database run in Frankfurt (europe-west3). Your request and your data stay on EU infrastructure end to end.
Read-onlyGET is the only method. The API cannot modify, delete or write anything.
TenancyA key reads exactly one organization, resolved server-side from the key itself. No caller parameter can move it.
KeysIssued once, stored only as SHA-256 hashes, revocable in one call, scoped deny-by-default.
Health dataWellness is a separate scope granted only on written instruction. Rows are name-free and per-athlete consent withdrawal is enforced in the API itself.
AuditEvery request is logged: key prefix, endpoint, parameters, row count, status and origin, retained for 365 days for your controller audits.
MinimizationSerializers are explicit whitelists; contact details, account identifiers and free-text coach notes are never emitted.

For the current annex, write to eric@makoapp.io.

#

OpenAPI spec

The v1 contract in OpenAPI 3.1: every endpoint, parameter, row schema and error shape. Use it to generate a typed client or to wire the API into your tooling.

openapi.yaml