Skip to content

Tests

A test is a structured performance evaluation: a lactate test, a VO2max test, an FTP test, or any similar protocol that produces a defined set of physiological markers. Tests live alongside activities and traces, and either can reference a test.

Where a trace is a single point-in-time value and an activity is a continuous recording, a test is the structured output of a deliberate evaluation protocol. The shape holds both interoperable, consumer-facing thresholds (first and second threshold, FatMax) and methodology-specific outputs (LT1 and LT2, VT1 and VT2, MLSS).

Test results

The results field on a test is a JSON object. Every field is optional. A test can carry any combination.

Thresholds (consumer-facing)

These are interoperable across testing methodologies. Most apps built on SweatStack should read from these.

Field Type Description
first_threshold Marker The aerobic threshold (LT1 or VT1, depending on the protocol).
second_threshold Marker The anaerobic threshold (LT2, VT2 or MLSS, depending on the protocol).
fatmax Marker The point of maximal fat oxidation.

Tests feed the metabolic map

A test's results become measured markers on the athlete's metabolic map for the test's sport, read per metric from the Marker fields (power, speed, heart_rate) and from the scalar fields (critical_power, heart_rate_max, vo2max, ...). A fresh test (within 90 days; 180 for vo2max) outranks SweatStack's own model; an older one yields to it. Fill the consumer-facing fields yourself. mlss, lt2 and vt2 are published on the map as what they are and never fill second_threshold for you; the same holds for lt1, vt1 and first_threshold. You ran the protocol, so you say which result is the athlete's threshold.

Thresholds (methodology-specific)

For testing protocols where the underlying methodology matters.

Field Type Description
lt1 Marker First lactate threshold.
lt2 Marker Second lactate threshold.
vt1 Marker First ventilatory threshold.
vt2 Marker Second ventilatory threshold.
mlss Marker Maximal lactate steady state.

Capacity and ceiling values

Field Type Unit Description
vo2max float mL/min Confirmed VO2 plateau.
vo2peak float mL/min Highest observed VO2.
vlamax float mmol/L/s Maximal lactate accumulation rate.
heart_rate_max int bpm Observed maximal heart rate.
critical_power int watts Critical power (cycling).
critical_speed float m/s Critical speed (running).
w_prime float kJ W′ (anaerobic work capacity).
d_prime float meters D′ (anaerobic distance capacity).

Economy and efficiency

Field Type Unit Description
economy float mL O₂/kg/km Running or cycling economy.
efficiency float % Gross mechanical efficiency.

Marker shape

A marker is a point on the intensity-duration curve, expressed in any of the metrics SweatStack supports.

{
    "power": 285,
    "heart_rate": 168,
    "speed": 4.2,
    "lactate": 4.0,
    "vo2": 3200
}

Every field is optional. Fill in the ones the testing protocol measured.

Create a test

POST /api/v1/tests/ creates a test.

from datetime import datetime
from zoneinfo import ZoneInfo

import sweatstack
from sweatstack.openapi_schemas import Marker, TestResults
from sweatstack import Client

client = Client()

test = client.tests.create(
    sport="cycling.road",
    start=datetime(2026, 5, 6, 10, 0, tzinfo=ZoneInfo("Europe/Amsterdam")),
    end=datetime(2026, 5, 6, 11, 30, tzinfo=ZoneInfo("Europe/Amsterdam")),
    title="Lactate step test",
    results=TestResults(
        first_threshold=Marker(power=220, heart_rate=150, lactate=2.0),
        second_threshold=Marker(power=285, heart_rate=168, lactate=4.0),
        vo2max=4100,
    ),
    tags=["lab", "step-test"],
)

tests.create returns the created test. A naive start or end raises ValueError before the request is sent.

curl -X POST "https://app.sweatstack.no/api/v1/tests/" \
    -H "Authorization: Bearer {your_access_token}" \
    -H "Content-Type: application/json" \
    -d '{
        "title": "Lactate step test",
        "sport": "cycling.road",
        "start": "2026-05-06T10:00:00+02:00",
        "end": "2026-05-06T11:30:00+02:00",
        "results": {
            "first_threshold": {"power": 220, "heart_rate": 150, "lactate": 2.0},
            "second_threshold": {"power": 285, "heart_rate": 168, "lactate": 4.0},
            "vo2max": 4100
        },
        "tags": ["lab", "step-test"]
    }'

Body fields:

  • sport (required): an Open Sport Taxonomy code. An unrecognized code returns 422 with code invalid_sport.
  • start (required): when the test started. An offset-aware ISO 8601 datetime, for example 2026-05-06T10:00:00+02:00 or ...Z. A datetime without an offset returns 422.
  • end (optional): when the test ended, in the same format. If you omit it, SweatStack sets it to start plus 3 hours.
  • title (optional): a free-text name.
  • results (optional): the object described under Test results.
  • tags (optional): a list of strings.

Requires the data:write scope.

The API returns the created test. SweatStack stores the offset you sent, and every read returns start and end as UTC alongside the local start_local and end_local. See Timezones.

List tests

GET /api/v1/tests/ returns tests, filtered by date range, sport, tags and creator.

from datetime import date

from sweatstack import Client

client = Client()

tests = client.tests.list(start=date(2026, 1, 1), sport=["cycling.road"])

tests.list takes the same filters as the endpoint, plus output="pandas" (or "polars") to return a DataFrame.

curl -X GET "https://app.sweatstack.no/api/v1/tests/?start=2026-01-01&sport=cycling.road" \
    -H "Authorization: Bearer {your_access_token}"

Query parameters:

  • start (optional): first day of the range, YYYY-MM-DD. Included. Matches each test's own local date.
  • end (optional): last day of the range, YYYY-MM-DD. Included.
  • sport (optional): a sport code. Repeat the parameter for more than one sport: ?sport=cycling.road&sport=running.road.
  • tags (optional): a tag. Repeat the parameter for more than one tag.
  • created_by (optional): the ID of the user or app that created the test.
  • limit (optional, default 50): the maximum number of tests to return.
  • offset (optional, default 0): the number of tests to skip, for paging.

Requires the data:read scope.

Get a single test

GET /api/v1/tests/{test_id} returns the test, plus the traces and activities that belong to it.

from sweatstack import Client

client = Client()

test = client.tests.retrieve(test_id)
linked_only = client.tests.retrieve(test_id, trace_resolution="linked")

tests.retrieve maps trace_resolution to the traces query parameter.

curl -X GET "https://app.sweatstack.no/api/v1/tests/{test_id}" \
    -H "Authorization: Bearer {your_access_token}"

Query parameters:

  • traces (optional, default auto): which traces the response includes. auto returns the traces whose timestamp falls inside the test's time range, plus traces explicitly linked to this test through their test_id, minus traces explicitly linked to a different test. linked returns only the explicitly linked traces, whatever their timestamp.

Activities are always matched by time overlap. traces does not affect them.

Requires the data:read scope.

The API returns 404 when the test does not exist.

Update a test

PUT /api/v1/tests/{test_id} replaces the whole test. It is not a patch: every optional field you leave out is set to null. To change one field, read the test first and send every field back.

from datetime import datetime
from zoneinfo import ZoneInfo

import sweatstack
from sweatstack.openapi_schemas import Marker, TestResults
from sweatstack import Client

client = Client()

client.tests.replace(
    test_id,
    sport="cycling.road",
    start=datetime(2026, 5, 6, 10, 0, tzinfo=ZoneInfo("Europe/Amsterdam")),
    title="Lactate step test (revised)",
    results=TestResults(second_threshold=Marker(power=290, heart_rate=170, lactate=4.0)),
)

tests.replace takes the same arguments as tests.create.

curl -X PUT "https://app.sweatstack.no/api/v1/tests/{test_id}" \
    -H "Authorization: Bearer {your_access_token}" \
    -H "Content-Type: application/json" \
    -d '{
        "title": "Lactate step test (revised)",
        "sport": "cycling.road",
        "start": "2026-05-06T10:00:00+02:00",
        "results": {
            "second_threshold": {"power": 290, "heart_rate": 170, "lactate": 4.0}
        }
    }'

The body takes the same fields as Create a test.

Requires the data:write scope.

Errors:

  • 403: a different app created the test, and your token has no access to it.
  • 404: the test does not exist.

Delete a test

DELETE /api/v1/tests/{test_id} deletes the test.

from sweatstack import Client

client = Client()

client.tests.delete(test_id)
curl -X DELETE "https://app.sweatstack.no/api/v1/tests/{test_id}" \
    -H "Authorization: Bearer {your_access_token}"

Requires the data:write scope.

Errors are the same as for Update a test: 403 and 404.

App metadata

Your app can attach its own JSON to a test with PUT /api/v1/tests/{test_id}/app-metadata and delete it with DELETE /api/v1/tests/{test_id}/app-metadata. See Application metadata for the full pattern. It is the same for activities, traces and tests.