Skip to content

Workouts

Beta

The workouts API is in beta and is subject to breaking changes. If you plan to use it in production, email info@sweatstack.no before you integrate, so we can tell you about changes in advance and help you migrate.

SweatStack workouts are planned 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 did.

When you create a scheduled workout, SweatStack stores it and pushes it to the athlete's connected platforms, so the athlete can run it from their wearable.

Garmin Connect is the only integration that imports workouts today. It delivers running, cycling, and pool swimming. Open-water swimming is the one exception (Garmin's Training API has no sport value for it), so the API rejects it at create time instead of dropping it silently. The integrations support matrix tracks which platforms and sports are live. It grows without any change on your side.

What you can do today

  • Create a workout scheduled for a specific date.
  • List the scheduled workouts of an athlete.
  • Read a single scheduled workout by id.
  • Update a scheduled workout in place. SweatStack pushes the change to the connected integrations.
  • Delete a scheduled workout. SweatStack also removes it from the connected integrations.

What will be added later

  • Workout library workouts: workouts without a start date, used as templates.

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, its field reference, examples, and validation tooling.

Two things are SweatStack-specific, and they are the important part of this page:

  1. SweatStack accepts only a subset of SWF (the table below). If a workout uses anything outside it, the API rejects it 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 (for example 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 and 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 (for example {"percent": 90, "of": "ftp"}), resolved from parameters at create time.
pool_length The top-level SWF field, in meters. Required for a pace-targeted pool swim (Garmin needs it). Optional otherwise.

The subset grows as SweatStack expands what it can fan out. Email info@sweatstack.no if you have a use case that is not supported yet.

Create a scheduled workout

POST /api/v1/workouts/schedule creates a workout and queues it for delivery.

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}
    }'

Body fields. All three are SweatStack-specific and not part of SWF:

  • swf (required): the SWF document. SweatStack reads the sport from swf.sport; there is no separate sport field on the envelope.
  • start (required): the athlete's local wall-clock, naive, with no offset, for example 2026-05-25T07:00:00. A scheduled workout is a plan, so it has no absolute instant and no timezone. SweatStack schedules it on the calendar date of that local time.
  • parameters (optional): the values for the ParameterRefs in the SWF document, for example {"ftp": 250}. SweatStack does not derive them from athlete data; you provide them. If the workout references a parameter (ftp, max_hr, threshold_pace, and so on) that you do not provide, the API rejects the request with missing_parameters. Omit parameters for workouts that use only absolute values or zones. Zones need no parameters.

Requires the workouts:write scope.

The API returns 201 with the stored workout. See Success response.

Validation errors

SweatStack validates the workout at create time. A failure returns 422 Unprocessable Entity with a 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 codes you are most likely to see:

Code Meaning
unsupported_sport The sport is not in the accepted set. This includes swimming.open_water.
unsupported_intensity The intensity quantity is not in the accepted set, for example RPE.
unsupported_shape The shape is not supported.
unsupported_zone_quantity A zone target on a quantity with no zone model, for example 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 code 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 and application_name identify the app that created the workout.
  • deliveries is the per-integration delivery status (see below). Every read (GET and list) carries it too.
  • warnings is a per-integration compatibility report. It appears only on the create and update responses; a GET does not carry it. See below.

List scheduled workouts

GET /api/v1/workouts/schedule/ returns the athlete's scheduled workouts.

curl -X GET "https://app.sweatstack.no/api/v1/workouts/schedule/?application_id={your_application_id}" \
    -H "Authorization: Bearer {your_access_token}"

Query parameters:

  • application_id (optional): return only the workouts created by this app.
  • limit (optional, default 50): the maximum number of workouts to return.
  • offset (optional, default 0): the number of workouts to skip, for paging.

Requires the workouts:read scope. So does GET /api/v1/workouts/schedule/{id}, which returns one workout.

Each item has the same shape as the create response, without warnings.

Update a scheduled workout

PUT /api/v1/workouts/schedule/{id} takes the same envelope as create. SweatStack validates the workout again, replaces its swf, start and parameters, and delivers the new version to the connected integrations in place: the integration-side workout is updated, not deleted and recreated, so on Garmin the calendar entry and its id stay the same. The response has the same shape as the create response, including warnings. deliveries resets to pending while SweatStack pushes the update.

Requires the workouts:write scope.

Delete a scheduled workout

DELETE /api/v1/workouts/schedule/{id} returns 204 No Content. SweatStack also removes the workout from the connected integrations, for example from the athlete's Garmin calendar. That cleanup is asynchronous and not guaranteed: the delete succeeds immediately, and if the device side fails, the worst case is a stale entry on the device.

Requires the workouts:write scope.

Delivery status and fanout

After SweatStack stores 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 The integration, for example garmin_connect.
status pending (accepted and queued), delivered, or not_delivered.
reason Set when status is not_delivered: missing_permission, no_integration, incompatible, not_scheduled or error. Otherwise null.
detail Optional human-readable detail. null when there is none.
updated_at When this entry last changed.

A 201 or 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 report, computed when you submit the workout: issues the target cannot represent, and adjustments it will apply (for example, Garmin flattens a deeply nested workout). 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 swf payload: schema, field reference, examples, and tooling.
  • Activities. The recorded counterpart of a workout.
  • Integrations. Which platforms receive workouts (Garmin Connect today) and which sports they cover.