API reference
Every endpoint on the platform API, generated from the spec the API publishes about itself.
- Base URL
- https://api.stridee.com
- Auth
- An Ed25519 signature over every request — there is no bearer token. How to sign.
- Spec
- /platform/openapi.json — OpenAPI 3.1, if you would rather generate a client than read this.
/v1/accounts
List your users.
Everyone you have ever named to us, including people who were sent a link and
never came back through it — those have no connections and appear nowhere in
GET /v1/connections, which is the difference between the two endpoints. One
row per person, most recently active first.
Paged with a cursor: send starting_after = the id of the last row you
received, and keep going while has_more. Most recently active first means
the order changes as your users train, so a page is a position in the list
rather than a count into it — which is exactly what offset could not be.
Query parameters
| Field | Type | Description |
|---|---|---|
| external_user_id | string | Narrow to exactly one of your users, by your id for them. The way back from an id in your database to ours. |
| starting_after | string · uuid | The |
| limit | integer · int64 | Defaults to 50, capped at 200. |
Responses
| Field | Type | Description |
|---|---|---|
| accountsRequired | AccountView[] | |
| has_moreRequired | boolean | Whether another page exists. Keep going while it is true. |
| next_starting_after | string · uuid | What to send as |
| total | integer · int64 | How many match the filter, on the first page only; |
{
"accounts": [
{
"activities": 0,
"connected_at": null,
"created_at": "2026-08-05T11:44:02Z",
"external_user_id": "user_4821",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"last_event_at": null,
"providers": [
"string"
],
"status": "active"
}
],
"has_more": true,
"next_starting_after": null,
"total": null
}One user: their connections, and what has happened to them.
Connections here include revoked ones, unlike GET /v1/connections — "they
disconnected COROS on Tuesday" is the answer to the question that actually
gets asked.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | Our id for the user, as returned by |
Responses
| Field | Type | Description |
|---|---|---|
| connectionsRequired | ConnectionView[] | Every connection, including revoked ones. "They disconnected COROS on Tuesday" is the answer to the support question that actually gets asked, and a list that dropped the row would show an athlete who appears never to have connected anything. |
| created_atRequired | string · date-time | |
| eventsRequired | EventView[] | Their recent events, newest first — the same rows the stream page shows, narrowed to this person. |
| external_user_idRequired | string | |
| idRequired | string · uuid |
{
"connections": [
{
"connected_at": "2026-08-05T11:44:02Z",
"external_user_id": "user_4821",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"provider": "coros",
"revoked_at": null,
"scope": "workout",
"status": "active",
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}
],
"created_at": "2026-08-05T11:44:02Z",
"events": [
{
"created_at": "2026-08-05T11:44:02Z",
"data": "string",
"delivered": 0,
"deliveries": 0,
"end_user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"manual": true,
"provider": "coros",
"type": "string"
}
],
"external_user_id": "user_4821",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85"
}/v1/accounts/{id}
Change your id for one of your users.
For when your own user table was re-keyed and the id you linked somebody
under is not the id you have any more. Our user_id does not change, so no
connection moves, no activity grant is affected, no event is rewritten, and
nobody goes back through consent. The next POST /v1/connect with the new id
finds this person rather than creating a second one.
This is a rename, not a merge. If another of your users already goes by the id you send, you get a 409 and nothing changes — deciding what should happen to two sets of connections at the same provider is not a guess this API is willing to make on your behalf.
If what you actually want is to stop referring to someone at all, that is
DELETE /v1/accounts/{id}.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | Our id for the user, as returned by |
Request body
| Field | Type | Description |
|---|---|---|
| external_user_idRequired | string | What you want to call this person from now on. |
{
"external_user_id": "user_4821"
}Responses
| Field | Type | Description |
|---|---|---|
| created_atRequired | string · date-time | |
| external_user_idRequired | string | Yours, as it now stands. |
| idRequired | string · uuid | Ours. Unchanged by a rename — that is the point of one. |
| updated_atRequired | string · date-time |
{
"created_at": "2026-08-05T11:44:02Z",
"external_user_id": "user_4821",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"updated_at": "2026-08-05T11:44:02Z"
}/v1/accounts/{id}
Stop referring to one of your users.
This does not delete a person, and it cannot. What it deletes is your reference to them: your id for them, their authorizations through you, the consents behind those, any outstanding management link, their event history on your stream, and your permission to read the activities they shared with you. Their workouts are not yours and stay exactly where they are — as does every other developer's separate reference to the same athlete, which you could never see and still cannot.
Live connections are revoked first, so the athlete's grant at the provider is withdrawn on the way out under the usual rule: only if this was their last connection through Stridee, since the same person may use another product that also integrates with us.
You get one account.deleted event rather than an account.disconnected per
connection — a handler seeing four disconnections and then silence cannot
tell that from someone who merely unplugged everything. It carries the ids in
data and has no user_id of its own, because the user it would name no
longer exists.
Afterwards the id is free. POST /v1/connect with the same
external_user_id creates a new person with a new user_id, connected to
nothing, and none of the old history comes back.
Idempotent in the only sense that matters: deleting someone already deleted is a 404.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | Our id for the user, as returned by |
Responses
List the activities you have been granted.
Every workout one of your users was connected to you for, newest first — the
same rows that were announced to your endpoint as activity.created, in the
same shape. This is the reconciliation path: if a delivery was missed, this
is how you find it, and file.url is the same download URL the event
carried.
Page against received_at, not start_time. Providers backfill and
correct, so a workout from March can arrive today; the order here is the
order you were told, which is the only one that is monotonic. since and
until window that same axis, half-open, so a nightly job can ask for
since=<last run> and get exactly what it has not seen.
Paged with a cursor: send starting_after = the id of the last row you
received, and keep going while has_more. next_starting_after is that id,
so a loop never has to reach into the array. A cursor names a row rather than
a distance from one, so activities arriving while you page cannot shift the
boundary under you — which an offset would, and did.
Access is the grant that was recorded when each activity arrived — the same
rule GET /v1/activities/{id}/file applies. Disconnecting stops new
activities; it does not remove the ones from the period your user had agreed
to share, so they stay listed here. An activity whose provider offered no
downloadable file has "file": null, and so does one whose bytes have not
landed yet.
Query parameters
| Field | Type | Description |
|---|---|---|
| user_id | string · uuid | Narrow to one athlete, by our id for them — what |
| provider | string |
|
| since | string | Only activities granted at or after this RFC 3339 instant. |
| until | string | Only activities granted strictly before it. Half-open, so one window's
|
| starting_after | string · uuid | The |
| limit | integer · int64 | Defaults to 50, capped at 200. |
Responses
| Field | Type | Description |
|---|---|---|
| activitiesRequired | ActivityView[] | |
| has_moreRequired | boolean | Whether another page exists. The loop is |
| next_starting_after | string · uuid | What to send as |
| total | integer · int64 | How many match the filter, on the first page only; Not a paging control — |
{
"activities": [
{
"device": null,
"external_user_id": "user_4821",
"file": "string",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"name": null,
"provider": "coros",
"provider_activity_id": "string",
"received_at": "2026-08-05T11:44:02Z",
"sport": null,
"start_time": null,
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}
],
"has_more": true,
"next_starting_after": null,
"total": null
}Download the raw file for an activity.
The data.file.url on an activity.created points here. It is an ordinary
signed request — sign it exactly the way you sign every other call — and it
answers 302 with a short-lived URL that needs no credential of its own.
Follow the redirect; do not store what it points at. That URL expires in
minutes, while the one in the event body does not, so keep the event's and
call this again whenever you need the bytes.
What you get is what the device wrote: the provider's own FIT, TCX or GPX,
unmodified and unparsed. data.file.format on the event tells you which, and
the redirect target carries the matching content type.
Access is the grant that was recorded when the activity arrived, so this
answers for exactly the workouts your user was connected to you for.
Disconnecting stops new activities; it does not take away files from the
period they had agreed to share. An activity whose provider offered no
downloadable file — "file": null on the event — has nothing to serve and
answers 404.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | The activity, as |
Responses
/v1/connect
Start a link.
Request body
| Field | Type | Description |
|---|---|---|
| external_user_idRequired | string | Your id for this person, from your own database. The only identifier you have to send, and the only one you have to store. Call this twice for the same value and you get the same user back rather than a second one — which is what makes retrying safe. |
| providerRequired | string |
|
| return_uri | string | Where to send their browser when consent is finished, with Must be registered on your account first ( |
{
"external_user_id": "user_4821",
"provider": "coros",
"return_uri": "https://app.yourapp.com/settings/devices"
}Responses
| Field | Type | Description |
|---|---|---|
| connect_urlRequired | string | Send your user here. Redirect their browser — this is a consent screen a person has to read, not something to fetch server-side. It is a page we host, not the provider's authorize endpoint. It names you, names us, and carries our terms, because your user is about to authorize their watch data flowing through infrastructure they have never heard of and every provider agreement behind this API assumes they were told. One click later they are at the provider, and the rest of the flow is as it was. The name your user reads on it is the one on your Stridee account. Without one the page still works and simply names no product. |
| expires_atRequired | string · date-time | When |
| user_idRequired | string · uuid | Our id for the person you named. Stable across every call for the same
|
{
"connect_url": "https://api.stridee.com/connections/start?state=…",
"expires_at": "2026-08-05T11:44:02Z",
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}List connections.
Live authorizations, newest first — one row per watch rather than per person, so somebody who linked Garmin and COROS is two.
This is a page. It used to be every connection on the account in one
response, which is fine at ten and is several megabytes at ten thousand.
Send starting_after = the id of the last row you received and keep going
while has_more; next_starting_after carries that id for you. A revoked
connection is not in this list and cannot be a cursor into it.
Query parameters
| Field | Type | Description |
|---|---|---|
| external_user_id | string | Narrow to one of your users. Omit for every connection on the account. |
| starting_after | string · uuid | The |
| limit | integer · int64 | Defaults to 50, capped at 200. |
Responses
| Field | Type | Description |
|---|---|---|
| connectionsRequired | ConnectionView[] | |
| has_moreRequired | boolean | Whether another page exists. Keep going while it is true. |
| next_starting_after | string · uuid | What to send as |
| total | integer · int64 | How many live connections match, on the first page only; |
{
"connections": [
{
"connected_at": "2026-08-05T11:44:02Z",
"external_user_id": "user_4821",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"provider": "coros",
"revoked_at": null,
"scope": "workout",
"status": "active",
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}
],
"has_more": true,
"next_starting_after": null,
"total": null
}/v1/connections/{id}
Disconnect.
The connection stops working immediately and its tokens are dropped.
Your access ends here. The athlete's authorization at the provider is only withdrawn if this was their last connection through Stridee — the same person may use two products that both integrate with us, and revoking the grant on your behalf would take the other one down without either of you knowing.
Idempotent: disconnecting something already disconnected is a 404, not a second revocation.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | The connection to disconnect, as returned by |
Responses
Mint a link that lets one of your users manage their own connections.
The page it opens lists that person's live connections with you and lets them disconnect any of them. It shows nothing about any other developer they may also be connected through, and disconnecting here does not touch those.
Why this exists rather than just DELETE /v1/connections/{id}: the athlete
has no Stridee account and never will, so there is nothing to log them in as.
The link is the credential. If your product already has its own settings
screen, use the DELETE and skip this entirely.
Request body
| Field | Type | Description |
|---|---|---|
| external_user_idRequired | string | Your id for the person, the same value you passed to |
{
"external_user_id": "user_4821"
}Responses
| Field | Type | Description |
|---|---|---|
| expires_atRequired | string · date-time | |
| manage_urlRequired | string | Put this in front of your user — a settings screen, an email, a support reply. Anyone holding it can disconnect that person's watches from your product, so treat it like a password reset link rather than a page URL. |
{
"expires_at": "2026-08-05T11:44:02Z",
"manage_url": "https://api.stridee.com/connections/manage?token=…"
}/v1/wellness
List the health summaries you have been granted.
Every sleep, daily, HRV, stress and fitness record one of your users was
connected to you for, newest first — the same rows that were announced to
your endpoint as wellness.created, in the same shape. This is the
reconciliation path: if a delivery was missed, this is how you find it.
Two different axes, and they answer different questions. calendar_date
(with from_date/to_date) is what the measurement is about, and it is
what you want for "show me last week's sleep". received_at (with
since/until) is when we told you, it is what this listing is ordered on,
and it is the only one that is monotonic — providers backfill and correct, so
a night from March can arrive today. Page and reconcile on received_at;
query and display on calendar_date.
Summaries change. Unlike an activity, a measurement can be revised after
you are told about it: Garmin sends a night tentatively and settles it hours
later. revision starts at 1 and increments whenever the payload actually
changed, and a revision arrives as wellness.updated rather than a second
wellness.created. Upsert on id and take the higher revision.
summary is the whole provider payload except its time series. The
minute-by-minute data — heart-rate samples, sleep stages, the stress curve —
is behind series_url, because it is around ninety percent of the bytes and
almost nothing needs it. A row whose series_url is null has none at all.
Access is the grant that was recorded when each summary arrived. Disconnecting stops new ones; it does not remove those from the period your user had agreed to share.
Query parameters
| Field | Type | Description |
|---|---|---|
| user_id | string · uuid | Narrow to one athlete, by our id for them — what |
| provider | string |
|
| kind | string |
|
| from_date | string | Only summaries for this calendar day or later. Inclusive. |
| to_date | string | Only summaries for this calendar day or earlier. Inclusive, unlike
|
| since | string | Only summaries granted at or after this RFC 3339 instant. |
| until | string | Only summaries granted strictly before it. Half-open, so one window's
|
| starting_after | string · uuid | The |
| limit | integer · int64 | Defaults to 50, capped at 200. |
Responses
| Field | Type | Description |
|---|---|---|
| has_moreRequired | boolean | Whether another page exists. Exact — it comes from a row we fetched and discarded rather than from arithmetic on a count. |
| wellnessRequired | WellnessView[] | |
| next_starting_after | string · uuid | What to send as |
| total | integer · int64 | How many match the filter, on the first page only; |
{
"has_more": true,
"next_starting_after": null,
"total": null,
"wellness": [
{
"calendar_date": "string",
"duration_seconds": null,
"external_user_id": "user_4821",
"finality": null,
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"kind": "string",
"metrics": {
"active_kilocalories": null,
"active_time_seconds": null,
"avg_hr": null,
"avg_stress_level": null,
"awake_seconds": null,
"bmr_kilocalories": null,
"body_battery": null,
"body_battery_charged": null,
"body_battery_drained": null,
"body_fat_percent": null,
"body_mass_index": null,
"body_water_percent": null,
"bone_mass_grams": null,
"deep_sleep_seconds": null,
"diastolic": null,
"distance_meters": null,
"epoch_activity_type": null,
"epoch_intensity": null,
"fitness_age": null,
"fitness_age_enhanced": null,
"floors_climbed": null,
"hrv_avg": null,
"hrv_rmssd": null,
"hrv_sdrr": null,
"light_sleep_seconds": null,
"max_hr": null,
"met": null,
"min_hr": null,
"moderate_intensity_seconds": null,
"muscle_mass_grams": null,
"nap_seconds": null,
"pulse": null,
"rem_sleep_seconds": null,
"respiration_avg": null,
"respiration_max": null,
"respiration_min": null,
"resting_hr": null,
"skin_temp_celsius": null,
"skin_temp_deviation_celsius": null,
"sleep_score": null,
"spo2_avg": null,
"spo2_min": null,
"spo2_on_demand": null,
"steps": null,
"systolic": null,
"total_sleep_seconds": null,
"vigorous_intensity_seconds": null,
"vo2_max": null,
"vo2_max_cycling": null,
"weight_grams": null
},
"provider": "coros",
"provider_summary_id": null,
"received_at": "2026-08-05T11:44:02Z",
"revised_at": null,
"revision": 0,
"series_url": null,
"start_time": "2026-08-05T11:44:02Z",
"summary": "string",
"tz_offset_seconds": null,
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}
]
}Fetch the time series behind one summary.
The minute-level data the listing deliberately leaves out: Garmin's
heart-rate samples on a daily, the sleep-stage map and respiration and SpO₂
traces on a sleep, the stress curve and body-battery events on a stress,
the beat-to-beat values on an hrv. Returned as the provider wrote it,
unmodified — the keys are theirs.
It is a separate call because it is around ninety percent of the payload's
bytes and most integrations never draw a curve. A fitness summary has none
at all, and neither does anything from COROS; those rows carry
"series_url": null and this answers 404 for them.
One 404 covers every way this can fail — someone else's summary, an id that
never existed, and one with no series — for the reason
GET /v1/activities/{id}/file gives: these ids travel in webhook bodies and
a status code that confirms an id exists is an oracle somebody can walk.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | The summary, as |
Responses
{}/v1/whoami
The endpoint a client is pointed at while it is being written.
It exists because the first signed request anybody makes is always wrong, and
the failures are hard to tell apart from outside: a clock, a stale key, a
@target-uri built with the wrong scheme, a body digested after a framework
re-encoded it. All four look like "401" against a real endpoint. Here the
only thing being tested is the signature, so the error is about the
signature — and a 200 means the client is correct, not that this particular
call happened to be.
It reads nothing and changes nothing, so it is also the one endpoint safe to leave in a customer's health check.
Responses
| Field | Type | Description |
|---|---|---|
| account_idRequired | string · uuid | The account the signature authenticated as. |
{
"account_id": "0b6f2d14-59c3-4e7a-8d05-1af38b62c9e7"
}/v1/workouts
The workouts on this account, newest first.
Ordered and paged on when the workout was created, not when it is
scheduled: the scheduled date is content, it can be edited or absent, and a
cursor over a value that moves either serves rows twice or steps over them.
from and to still filter on it, because "next week" is the question a
calendar asks.
Query parameters
| Field | Type | Description |
|---|---|---|
| user_id | string · uuid | One athlete, by our id for them. |
| from | string · date | Scheduled on or after this day, |
| to | string · date | Scheduled on or before this day. Inclusive — a calendar day is a unit you name, not a boundary between windows. |
| starting_after | string · uuid | The |
| limit | integer · int64 | Defaults to 50, capped at 200. |
Responses
| Field | Type | Description |
|---|---|---|
| has_moreRequired | boolean | Whether another page exists. Exact — it comes from a row we fetched and discarded, not from arithmetic on a count. |
| workoutsRequired | WorkoutView[] | |
| next_starting_after | string · uuid | What to send as |
| total | integer · int64 | How many match the filter, on the first page only. |
{
"has_more": true,
"next_starting_after": null,
"total": null,
"workouts": [
{
"created_at": "2026-08-05T11:44:02Z",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"name": "string",
"pool_length": "string",
"pushes": [
{
"error": null,
"external_id": null,
"provider": "coros",
"pushed_at": null,
"reason": null,
"status": "active"
}
],
"scheduled_date": null,
"sport": "string",
"steps": "string",
"updated_at": "2026-08-05T11:44:02Z",
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}
]
}/v1/workouts
Create a structured workout and push it to your user's device.
The workout is stored, then pushed, then answered for — so the pushes array
is what actually happened rather than what was intended.
Omit provider to reach every connected device that can carry the workout.
A provider that cannot represent it — Wahoo and a swim — comes back
unsupported beside the ones that took it, and the call still succeeds:
the workout exists and is on a watch. You get a non-2xx only when nothing
could have worked.
Request body
| Field | Type | Description |
|---|---|---|
| nameRequired | string | |
| sportRequired | Sport | |
| stepsRequired | Step[] | |
| user_idRequired | string · uuid | The athlete, by our id for them — what |
| pool_length | unknown | |
| provider | string | Which provider to push to. Omit to push to every connected provider that can carry this workout, which is what an athlete with two watches expects and what the first-party app has always done. |
| scheduled_date | string · date | The day this workout is for. Omit to leave it in the athlete's library without a calendar entry. |
{
"name": "string",
"pool_length": "string",
"provider": "coros",
"scheduled_date": null,
"sport": {},
"steps": [
{}
],
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}Responses
| Field | Type | Description |
|---|---|---|
| created_atRequired | string · date-time | |
| idRequired | string · uuid | |
| nameRequired | string | |
| pushesRequired | PushView[] | |
| sportRequired | string | |
| updated_atRequired | string · date-time | |
| user_idRequired | string · uuid | |
| pool_length | unknown | |
| scheduled_date | string · date | |
| steps | unknown | The steps as you sent them. Absent — rather than wrong — for a workout whose stored steps are not expressible in this contract. That cannot happen to a workout you authored; it is here because the same table holds first-party strength sessions, which are exercise blocks rather than intervals, and inventing an interval for one of those would be worse than omitting it. |
{
"created_at": "2026-08-05T11:44:02Z",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"name": "string",
"pool_length": "string",
"pushes": [
{
"error": null,
"external_id": null,
"provider": "coros",
"pushed_at": null,
"reason": null,
"status": "active"
}
],
"scheduled_date": null,
"sport": "string",
"steps": "string",
"updated_at": "2026-08-05T11:44:02Z",
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}One workout, and where it got to.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | The workout id |
Responses
| Field | Type | Description |
|---|---|---|
| created_atRequired | string · date-time | |
| idRequired | string · uuid | |
| nameRequired | string | |
| pushesRequired | PushView[] | |
| sportRequired | string | |
| updated_atRequired | string · date-time | |
| user_idRequired | string · uuid | |
| pool_length | unknown | |
| scheduled_date | string · date | |
| steps | unknown | The steps as you sent them. Absent — rather than wrong — for a workout whose stored steps are not expressible in this contract. That cannot happen to a workout you authored; it is here because the same table holds first-party strength sessions, which are exercise blocks rather than intervals, and inventing an interval for one of those would be worse than omitting it. |
{
"created_at": "2026-08-05T11:44:02Z",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"name": "string",
"pool_length": "string",
"pushes": [
{
"error": null,
"external_id": null,
"provider": "coros",
"pushed_at": null,
"reason": null,
"status": "active"
}
],
"scheduled_date": null,
"sport": "string",
"steps": "string",
"updated_at": "2026-08-05T11:44:02Z",
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}/v1/workouts/{id}
Replace a workout and re-push it.
Providers that can update in place do — Garmin keeps the same workoutId, so
the athlete's calendar entry shows the new content rather than gaining a
duplicate beside it.
This re-pushes to every capable connected provider, including ones this workout never reached. That is deliberate and it is what the first-party app does: a workout created before your user connected their watch should land on it when they edit, rather than staying invisible forever because the original push had nowhere to go.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | The workout id |
Request body
| Field | Type | Description |
|---|---|---|
| nameRequired | string | |
| sportRequired | Sport | |
| stepsRequired | Step[] | |
| pool_length | unknown | |
| scheduled_date | string · date | Settable to |
{
"name": "string",
"pool_length": "string",
"scheduled_date": null,
"sport": {},
"steps": [
{}
]
}Responses
| Field | Type | Description |
|---|---|---|
| created_atRequired | string · date-time | |
| idRequired | string · uuid | |
| nameRequired | string | |
| pushesRequired | PushView[] | |
| sportRequired | string | |
| updated_atRequired | string · date-time | |
| user_idRequired | string · uuid | |
| pool_length | unknown | |
| scheduled_date | string · date | |
| steps | unknown | The steps as you sent them. Absent — rather than wrong — for a workout whose stored steps are not expressible in this contract. That cannot happen to a workout you authored; it is here because the same table holds first-party strength sessions, which are exercise blocks rather than intervals, and inventing an interval for one of those would be worse than omitting it. |
{
"created_at": "2026-08-05T11:44:02Z",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"name": "string",
"pool_length": "string",
"pushes": [
{
"error": null,
"external_id": null,
"provider": "coros",
"pushed_at": null,
"reason": null,
"status": "active"
}
],
"scheduled_date": null,
"sport": "string",
"steps": "string",
"updated_at": "2026-08-05T11:44:02Z",
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}/v1/workouts/{id}
Delete a workout and take it off the athlete's devices.
The provider objects go first, because the rows are the only record of what to remove — dropping them first would leave a workout on a watch with nothing left that knows about it. A provider that refuses the delete does not stop ours: the alternative is a workout nobody can remove because one provider is having a bad day.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | The workout id |
Responses
Something wrong or missing on this page? Tell us in Discord. Need something the API doesn’t do yet? Request it on the roadmap.