Workouts¶
Beta Functionality
The workouts API is in beta and breaking changes can land at short notice. If you plan to use this API in production, get in direct contact with us so we can warn you before a change ships and help you migrate. Email info@sweatstack.no before integrating, not after.
SweatStack workouts are planned training sessions: a structured prescription tied to a future start date. They're distinct from activities, which are recorded sessions. A workout is what the athlete plans to do. An activity is what they actually did.
When you create a scheduled workout, SweatStack stores it and pushes it to the athlete's connected platforms (for example Garmin Connect) so users can execute them directly from their wearables.
What you can do today¶
- Create a workout scheduled for a specific date.
- List the scheduled workouts for an athlete.
- Read a single scheduled workout by id.
- Update a scheduled workout in place (the change is re-pushed to connected integrations).
- Delete a scheduled workout (it is also removed from connected integrations).
What will be added later¶
- Workout library workouts (no
startdate, used as templates for later use).
The workout payload: SWF, and the subset SweatStack accepts¶
The workout content (the swf field) is a Structured Workout Format document. SWF is an open JSON format for prescribing structured workouts; we do not repeat its schema here, see structuredworkoutformat.dev for the format itself, its field reference, examples, and validation tooling.
Two things are SweatStack-specific and are the important part of this page:
- SweatStack accepts only a subset of SWF (the table below). Anything outside it is rejected at create time with a structured
422. - SweatStack wraps the SWF document in a small request envelope (
start,parameters) and adds a few fields to the response (deliveries,warnings,application_*). Those are described under Create and Delivery status.
Supported SWF subset¶
| Aspect | What SweatStack accepts |
|---|---|
| Sports | running, cycling, swimming and their sub-sports (e.g. running.trail, cycling.road, swimming.pool). swimming.open_water is not supported (Garmin's Training API cannot represent it). |
| Intensity quantity | speed (m/s), power (watts), heart_rate (bpm). RPE is not accepted. |
| Volume quantity | duration (seconds), distance (meters). |
| Shapes | constant, range, ramp, zone. |
| Zone targets | power (7-zone) and heart_rate (5-zone) only. Speed/pace zones are not supported (there is no standard model and Garmin cannot target one). A zone with a different of count is normalized to these models. |
| References | absolute values, and ParameterRef (e.g. {"percent": 90, "of": "ftp"}) resolved from parameters at create time. |
pool_length |
The top-level SWF field (meters). Required for a pace-targeted pool swim (Garmin needs it); optional otherwise. |
The subset grows as SweatStack expands what it can fan out. Get in touch if you have a use case that is not yet supported.
Create a scheduled workout¶
POST /api/v1/workouts/schedule takes an envelope with three top-level fields: swf, start, and an optional parameters map.
curl -X POST "https://app.sweatstack.no/api/v1/workouts/schedule" \
-H "Authorization: Bearer {your_access_token}" \
-H "Content-Type: application/json" \
-d '{
"swf": {
"version": "0.3.0",
"title": "Sweet Spot 2x20",
"sport": "cycling",
"content": [
{
"type": "repeat",
"count": 2,
"content": [
{
"type": "step",
"effort": "work",
"volume": {"type": "constant", "quantity": "duration", "value": 1200},
"intensity": {
"type": "range",
"quantity": "power",
"min": {"percent": 88, "of": "ftp"},
"max": {"percent": 93, "of": "ftp"}
}
},
{
"type": "step",
"effort": "rest",
"volume": {"type": "constant", "quantity": "duration", "value": 300}
}
]
}
]
},
"start": "2026-05-25T07:00:00",
"parameters": {"ftp": 250}
}'
The three envelope fields (all SweatStack-specific, not part of SWF):
startis the athlete's local wall-clock (naive, no offset), for example2026-05-25T07:00:00. A scheduled workout is a plan, so it has no absolute instant and no timezone. It is scheduled on the calendar date of that local time.parameterssupplies the values for the SWFParameterRefs. SweatStack does not derive them from athlete data; you provide them. Every parameter the workout references (e.g.ftp,max_hr,threshold_pace) must be present, or the request is rejected withmissing_parameters. Omitparametersfor workouts that use only absolute values or zones (zones need no parameters).- The
sportis read from theswf; there is no separate sport field on the envelope.
Validation errors¶
Validation runs against a single SweatStack-level strategy at create time. Failures return 422 Unprocessable Entity with a structured detail array; each entry has a code, a message, and a path into the workout tree.
{
"detail": [
{"code": "unsupported_sport", "message": "Sport 'swimming.open_water' is not supported by this platform", "path": []}
]
}
The most relevant codes:
| Code | Meaning |
|---|---|
unsupported_sport |
Sport not in the accepted set (including swimming.open_water). |
unsupported_intensity |
Intensity quantity outside the set (e.g. RPE). |
unsupported_shape |
Shape not supported. |
unsupported_zone_quantity |
A zone target on a quantity with no zone model (e.g. speed). |
missing_pool_length |
A pace-targeted pool swim without the top-level pool_length. |
pool_length_mismatch |
A swim distance that is not a whole number of pool_length lengths. |
missing_parameters |
The workout references ParameterRefs the request did not provide. detail[].missing lists them. This one is SweatStack-specific; the others come from SWF's compatibility check. |
The code and path semantics (and the full set of possible codes) are part of SWF; see structuredworkoutformat.dev.
Success response¶
POST (and PUT, see Update) return the stored workout plus a deliveries array and a warnings array:
{
"id": "01JW3K...",
"sport": "cycling",
"start": "2026-05-25T07:00:00",
"swf": { "..." },
"parameters": {"ftp": 250},
"application_id": "01JW3J...",
"application_name": "My App",
"deliveries": [
{"integration": "garmin_connect", "status": "pending", "reason": null, "detail": null, "updated_at": "2026-05-20T10:00:00Z"}
],
"warnings": []
}
application_id/application_nameidentify the third-party app that created the workout.deliveriesis the per-integration delivery status (see below). It appears on every read (GET, list) too.warningsis a per-integration compatibility heads-up, and appears only on the create and update responses (a plainGETdoes not carry it). See below.
Update a scheduled workout¶
PUT /api/v1/workouts/schedule/{id} takes the same envelope as create. It re-validates the workout, replaces its swf / start / parameters, and re-delivers it to connected integrations in place: the integration-side workout is updated, not recreated, so on Garmin the calendar entry and its id stay stable rather than flickering. The response is the same shape as create (with warnings), and deliveries resets to pending while the update is re-pushed.
Delete a scheduled workout¶
DELETE /api/v1/workouts/schedule/{id} returns 204 No Content. Deleting a workout also unschedules it from connected integrations (for example it is removed from the athlete's Garmin calendar). That cleanup is best-effort and asynchronous: the delete succeeds immediately, and a device-side failure at worst leaves a stale entry.
Delivery status and fanout¶
After SweatStack persists a create or update, an asynchronous task pushes the workout to the athlete's connected integrations. Two SweatStack-specific fields let you observe the outcome:
deliveries (on every read) reports one entry per integration the workout is fanned out to:
| Field | Meaning |
|---|---|
integration |
e.g. garmin_connect. |
status |
pending (accepted and queued), delivered, or not_delivered. |
reason |
Set when not_delivered, e.g. missing_permission, no_integration, incompatible, not_scheduled, error. |
detail |
Optional human-readable detail. |
updated_at |
When this entry last changed. |
A 201/200 therefore means "accepted and queued", not "on the device". Poll GET and read deliveries for the outcome. An empty deliveries array means the athlete has no connected integration that can import workouts.
warnings (on the create and update responses only) is a per-integration compatibility heads-up computed at submit time: issues the target cannot represent and adjustments it will apply (for example a deeply-nested workout that Garmin will flatten). Each entry mirrors SWF's {path, code, message} shape. Warnings never block the request; they tell you what a connected device will do with the workout before it happens.
See also¶
- API reference: Workouts. Every endpoint, request, and response schema, generated from the live OpenAPI spec.
- Structured Workout Format. The open spec for the
swfpayload: schema, field reference, examples, and tooling. - Activities. The recorded counterpart of a workout.
- Integrations. The Garmin connection that workouts fan out to.