Activities¶
Data Model
For more information about data models related to activity data, please refer to the Data Model documentation.
Response formats¶
Activity timeseries (lat/lon, heart rate, power, and similar at sample rate) is delivered as Apache Parquet, both per-activity (the activity data endpoint) and longitudinal. Activity summary and detail metadata (sport, distance, duration, and so on) are returned as JSON.
The Parquet timeseries has a few properties worth knowing before you build against it:
- Compression. Files are ZSTD compressed. Any Parquet reader with ZSTD support reads them, and most modern readers have it built in.
- Sample rate. Samples are usually at 1 Hz (one row per second), but this is not guaranteed: some devices and sports record at a different or variable rate. Read the timestamps rather than assuming a fixed interval.
- Shape. One row per sample, a timestamp column, and one column per metric. See the data model for the metric columns.
The Python SDK handles all of this for you and hands back a ready-to-use pandas DataFrame. If you read the Parquet bytes directly, see Reading the Parquet outside the Python SDK. If your stack doesn't have a good Parquet path, reach out. We're interested in hearing about your use case.
Activity metadata¶
Activity metadata contains data such as an id, sport type, start and end times, etc. of activities.
Retrieving metadata for activities is done by using the get_activities method.
import sweatstack
activities = sweatstack.get_activities()
By default, this will return a list of the last 100 activities. Check the reference for more options for filtering the returned activities.
Alternatively, to receive the activities as a pandas DataFrame, you can use the as_dataframe keyword argument.
activities = sweatstack.get_activities(as_dataframe=True)
The start and end columns are timezone-aware UTC timestamps, so they land in a single datetime64[ns, UTC] column and the usual pandas datetime operations work directly.
The DataFrame also includes naive-local start_local and end_local columns: the athlete's wall-clock time with the UTC offset already applied. Use those for display and for bucketing by local calendar day.
A common usecase for the returned pandas DataFrame is to apply some rolling operation on the activity start times, for example a 7-day rolling sum of the activity distances.
Roll on start_local so each activity falls in the local day the athlete actually trained.
activities.rolling("7d", on="start_local")["summary.distance"].sum()
Retrieve activity metadata by making a GET request to the activities endpoint:
curl -X GET "https://app.sweatstack.no/api/v1/activities/" \
-H "Authorization: Bearer {your_access_token}"
This returns the last 50 activities by default. You can customize the request with query parameters:
curl -X GET "https://app.sweatstack.no/api/v1/activities/?limit=100&offset=0&start=2024-01-01&end=2024-12-31&sports=running,cycling" \
-H "Authorization: Bearer {your_access_token}"
Available query parameters:
start- Filter activities after this date (YYYY-MM-DD format)end- Filter activities before this date (YYYY-MM-DD format)limit- Number of results to return (default: 50, no hard cap)offset- Pagination offset (default: 0)sport- Sport filter, repeatable for multiple sports (e.g.?sport=cycling&sport=running.road). Server-side; prefer this over filtering client-side.tags- Filter by tags, repeatable
Computing your own per-activity metrics? See custom calculations on activities. For most apps, you don't need a local database.
Response shape¶
The list response is an array of activity-summary objects. Realistic single-item example:
Example response (one activity)
[
{
"id": "act_01HV3KJD9M2P4S7T8WXQR0YZ",
"sport": "running.road",
"start": "2026-05-18T15:32:00Z",
"end": "2026-05-18T16:18:14Z",
"start_local": "2026-05-18T17:32:00",
"end_local": "2026-05-18T18:18:14",
"duration": "PT46M14S",
"metrics": ["power", "speed", "heart_rate", "cadence", "distance"],
"source_id": "src_01HV3KJB7XQ2N1F0G8YH9CDM",
"tags": ["easy"],
"summary": {
"power": { "mean": 248, "max": 412 },
"speed": { "mean": 3.5, "max": 4.9 },
"heart_rate": { "mean": 152, "min": 92, "max": 174, "start": 110, "end": 158 },
"distance": { "sum": 9712 },
"altitude": { "mean": 142, "gain": 78, "loss": 81, "min": 96, "max": 188 },
"cadence": { "mean": 178, "max": 192 }
},
"laps": [
{
"start": "2026-05-18T15:32:00Z",
"end": "2026-05-18T15:42:00Z",
"start_local": "2026-05-18T17:32:00",
"end_local": "2026-05-18T17:42:00",
"duration": "PT10M",
"power": { "mean": 232, "max": 305 },
"heart_rate": { "mean": 142, "min": 92, "max": 158, "start": 110, "end": 156 },
"distance": { "sum": 2050 }
}
],
"traces": null,
"app_metadata": null
}
]
The fields you'll use most:
| Path | Description |
|---|---|
id |
Stable activity ID. Use for /activities/{id}/... follow-up calls. |
sport |
Normalized SweatStack sport. See sport taxonomy. |
start, end |
Absolute UTC instant, ISO 8601 with a Z suffix. See Timezones. |
start_local, end_local |
Naive local wall-clock strings, convenient for display and date bucketing. |
duration |
ISO 8601 duration (computed from end - start). |
metrics |
Array of metric names recorded in the timeseries. Check before fetching. |
summary.* |
Per-metric aggregates. See summary statistics for the full set of paths. |
laps[] |
Per-lap summaries when the source recording had lap markers; same summary.* fields per lap. |
tags |
User-defined activity tags. |
Full schema: GET /api/v1/activities/ (in-site reference) · ActivitySummary, ActivitySummarySummary, Lap (schemas).
Run it live: open in the API playground. It uses your logged-in SweatStack session, so there's no API token to mint.
Activity data¶
Activity data is the timeseries data of an activity.
AISC downsampling
The segmentation_on parameters used below enable Adaptive Intensity Segmentation Codec (AISC): 20-50x smaller payloads with the mean-max curve and intensity distribution preserved within a bounded error. See it in action →
To get the activity data for an activity, you can use the get_activity_data method:
data = sweatstack.get_activity_data(activity_id)
Pass segmentation_on to segmentize with AISC (Adaptive Intensity Segmentation Codec), keyed on "power" or "speed":
data = sweatstack.get_activity_data(activity_id, segmentation_on="power")
The returned DataFrame is indexed by a timezone-aware UTC timestamp, with a timestamp_local column carrying the naive local wall-clock for each sample. This matches the longitudinal shape: merge and sort on the UTC index, group and bucket by local calendar date on timestamp_local. See Timezones.
Retrieve timeseries data for a specific activity by making a GET request:
curl -X GET "https://app.sweatstack.no/api/v1/activities/{activity_id}/data" \
-H "Authorization: Bearer {your_access_token}"
You can customize which metrics are returned and enable AISC segmentation:
curl -X GET "https://app.sweatstack.no/api/v1/activities/{activity_id}/data?metrics=duration,power,speed,heart_rate&segmentation_on=power" \
-H "Authorization: Bearer {your_access_token}"
Available query parameters:
metrics- Comma-separated list of metrics to include (default:duration,power,speed,heart_rate)segmentation_on- Downsample with AISC (Adaptive Intensity Segmentation Codec) keyed on a specific metric (powerorspeed)
Reading the Parquet outside the Python SDK¶
The endpoint returns a ZSTD-compressed Parquet body. If you're not using the Python SDK, read it with any Parquet library that supports the ZSTD codec.
import pandas as pd
# activity.parquet is the response body from GET /activities/{id}/data
df = pd.read_parquet("activity.parquet") # pyarrow handles ZSTD out of the box
import { parquetReadObjects } from "hyparquet";
import { compressors } from "hyparquet-compressors"; // adds ZSTD support
const buffer = await fetch(dataUrl, {
headers: { Authorization: `Bearer ${accessToken}` },
}).then((r) => r.arrayBuffer());
const rows = await parquetReadObjects({ file: buffer, compressors });
hyparquet reads Parquet in Node and the browser; the companion hyparquet-compressors package supplies the ZSTD codec. See the hyparquet docs for the full API.
Downloading the original file¶
Occasionally the normalized activity data won't surface a field that exists in the source recording. When that happens, GET /api/v1/activities/{activity_id}/download returns the original file from the provider (for example the FIT file), so you can read the raw field yourself.
Treat it as a fallback for the occasional gap, not the primary data path: for almost everything, the normalized data and timeseries endpoints are what you want.
Longitudinal data¶
Longitudinal data is activity data from multiple activities.
To get longitudinal data, you can use the get_longitudinal_data method:
data = sweatstack.get_longitudinal_data(
sports=["running"],
start=start_date,
)
Pass segmentation_on to downsample with AISC (Adaptive Intensity Segmentation Codec), keyed on "power" or "speed":
data = sweatstack.get_longitudinal_data(
sports=["cycling"],
start=start_date,
segmentation_on="power",
)
The returned DataFrame is indexed by a timezone-aware UTC timestamp, so samples from activities in different timezones sort and merge on one absolute axis. A timestamp_local column carries the naive local wall-clock for each sample, derived per-activity, so grouping by local calendar day stays correct across DST and travel. Merge and sort on the UTC index; group and bucket on timestamp_local. See Timezones.
Retrieve longitudinal data from multiple activities by making a GET request:
curl -X GET "https://app.sweatstack.no/api/v1/activities/longitudinal-data?sports=running&start=2024-01-01" \
-H "Authorization: Bearer {your_access_token}"
You can customize the request with additional parameters:
curl -X GET "https://app.sweatstack.no/api/v1/activities/longitudinal-data?sports=running,cycling&start=2024-01-01&end=2024-12-31&metrics=power,heart_rate&segmentation_on=power" \
-H "Authorization: Bearer {your_access_token}"
Available query parameters:
sports- Comma-separated list of sport types (required)start- Start date for filtering (YYYY-MM-DD format, optional)end- End date for filtering (YYYY-MM-DD format, optional)metrics- Comma-separated list of metrics to include (optional)segmentation_on- Segmentize with AISC (Adaptive Intensity Segmentation Codec) keyed on a specific metric (powerorspeed, optional)
Mean-max¶
The mean-max curve (also called the power-duration or speed-duration curve) is, for every duration, the highest average value of a metric that was sustained over any window of that length. It is the basis for power-duration profiling, and SweatStack can compute it both for a single activity and enveloped across many activities.
Per-activity mean-max¶
Use get_activity_mean_max for a single activity:
import sweatstack
mean_max = sweatstack.get_activity_mean_max(activity_id, metric="power")
Pass segmentation=True to segmentize with AISC (Adaptive Intensity Segmentation Codec) for visualization:
mean_max = sweatstack.get_activity_mean_max(activity_id, metric="power", segmentation=True)
curl -X GET "https://app.sweatstack.no/api/v1/activities/{activity_id}/mean-max?metric=power" \
-H "Authorization: Bearer {your_access_token}"
Available query parameters:
metric- The metric to compute the curve for (powerorspeed, required)segmentation- Segmentize with AISC (Adaptive Intensity Segmentation Codec) to reduce the number of points for visualization (optional)
Longitudinal mean-max¶
The longitudinal mean-max envelopes the curve across every activity matching a sport and date range, so each point is the best you sustained over the whole period.
Use get_longitudinal_mean_max:
mean_max = sweatstack.get_longitudinal_mean_max(
sports=["cycling"],
metric="power",
start="2024-01-01",
end="2024-12-31",
)
Without after, the returned DataFrame is indexed by the metric value (one row per intensity level).
curl -X GET "https://app.sweatstack.no/api/v1/activities/longitudinal-mean-max?sport=cycling&metric=power&start=2024-01-01&end=2024-12-31" \
-H "Authorization: Bearer {your_access_token}"
Available query parameters:
sport- Sport filter, repeatable for multiple sports (required)metric- The metric to compute the curve for (powerorspeed, required)start,end- Date range (YYYY-MM-DD)durations- Comma-separated durations in seconds to sample at (e.g.5,60,300,1200)after- One or more fatigue states (see below)by- Curve orientation (see below)
Curve orientation (by)¶
The by parameter selects which axis the curve is indexed on. The policy differs between the no-after and after cases, and by=duration is where everything is heading:
- Without
after: the curve is value-indexed andby=intensityis the default today. This will change:by=durationwill become the default, and in the future the only option. (by=durationcurrently requiresafter, so there is nothing to change in your no-aftercalls yet, just expect the default to flip in a later release.) - With
after:by=durationis the default and the only option going forward.by=intensityis a deprecated legacy orientation, still accepted for now but on its way out. Passby=durationexplicitly, or leavebyunset (it already resolves todurationformetric=power).
Deprecation
by=intensity is deprecated. For after calls, switch to by=duration now (it is the default and only supported orientation going forward). For no-after calls, by=duration will become the default and only option in a future release. The Python SDK raises a DeprecationWarning when you pass by="intensity" for an after call.
Fatigue states (after)¶
Passing one or more after values computes the mean-max over only the portion of each ride after that much accumulated work (kJ for power; metres for speed, experimental), then envelopes those across rides. This shows how your power-duration curve holds up under fatigue. Repeat after for multiple states (max 5); the response gains an after column and the date range is capped at 1 year.
With the default by=duration, the after response is index-free with columns duration, <metric>, activity_id, sport, after, the curve computed by a fast, exact segment kernel (sampled at durations if given, otherwise a default grid). For metric=speed, by=intensity remains the only orientation for now.
curl -X GET "https://app.sweatstack.no/api/v1/activities/longitudinal-mean-max?sport=cycling&metric=power&start=2024-01-01&end=2024-12-31&after=0&after=50&after=100" \
-H "Authorization: Bearer {your_access_token}"
Uploading files¶
SweatStack allows uploading activity files. At the moment only .fit files are supported (contact me if you need other formats).
Required scopes
Uploading files requires an API key with scope data:write, ensure you request this scope during user authorization. More info here.
Warning
This is not yet implemented in the Python client.
Upload activity files by making a POST request with multipart/form-data:
curl -X POST "https://app.sweatstack.no/api/v1/activities/upload" \
-H "Authorization: Bearer {your_access_token}" \
-F "[email protected]" \
-F "[email protected]"
Details:
- Method: POST
- Content-Type: multipart/form-data
- File limitation: Up to 10 files can be uploaded simultaneously
- Supported format: Currently only
.fitfiles are supported
Responses:
- Success: 202 Accepted
- Validation error: 422 Unprocessable Entity
After the request is accepted, the activities will be processed in the background and the processed data will be available in the Activity data endpoint. Processing of uploaded files typically takes a few seconds.
Backfill status¶
When a user first connects their account, SweatStack automatically backfills their historical activities. The backfill status endpoint provides real-time progress updates showing which activities have been loaded. This can for example be useful if you application needs at least 4 weeks of historical activity data to function properly.
Monitor backfill progress using the SweatStack client:
from datetime import datetime, timedelta
import sweatstack
four_weeks_ago = datetime.now() - timedelta(weeks=4)
# Loop over the status, new update every ~3s
for status in sweatstack.watch_backfill_status():
if status.backfill_loaded_until <= four_weeks_ago:
print(f"Backfilled loaded at least 4 weeks!")
break
# Or just check once
status = sweatstack.get_backfill_status()
print(f"Backfilled to: {status.backfill_loaded_until}")
The endpoint returns a streaming newline-delimited JSON (NDJSON) response with updates approximately every few seconds, automatically closing the connection after 60 seconds. The backfill_loaded_until timestamp moves backward in time as activities are backfilled from newest to oldest.
An error field is returned if the backfill status request fails.
Monitor backfill progress using cURL with streaming:
curl -N -X GET "https://app.sweatstack.no/api/v1/activities/backfill-status" \
-H "Authorization: Bearer {your_access_token}"
The -N flag disables buffering to see updates in real-time.
Example response (NDJSON stream):
{"backfill_loaded_until": "2024-03-01T10:00:00Z"}
{"backfill_loaded_until": "2024-02-15T08:30:00Z"}
{"backfill_loaded_until": "2024-01-01T12:00:00Z"}