Skip to content

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 start date, 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:

  1. SweatStack accepts only a subset of SWF (the table below). Anything outside it is rejected at create time with a structured 422.
  2. 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):

  • start is the athlete's local wall-clock (naive, no offset), for example 2026-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.
  • parameters supplies the values for the SWF ParameterRefs. 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 with missing_parameters. Omit parameters for workouts that use only absolute values or zones (zones need no parameters).
  • The sport is read from the swf; 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_name identify the third-party app that created the workout.
  • deliveries is the per-integration delivery status (see below). It appears on every read (GET, list) too.
  • warnings is a per-integration compatibility heads-up, and appears only on the create and update responses (a plain GET does 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