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 Parquet, both per-activity and longitudinal. Activity summary and detail metadata (sport, distance, duration, and so on) are returned as JSON.
If your tech 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 of the returned DataFrame are timezone aware.
Because these columns can contain datetime objects with different time zones and because pandas datatime64[ns] columns only support one timezone, the dtype of these columns is object.
For your convenience, start_local and end_local columns are also included in the returned DataFrame, containing the localized start and end times.
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.
This is a typical usecase for the start_local column, which contains the localized start times.
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-18T17:32:00+02:00",
"end": "2026-05-18T18:18:14+02:00",
"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-18T17:32:00+02:00",
"end": "2026-05-18T17:42:00+02:00",
"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 |
Timezone-aware ISO 8601. The TZ is the user's local zone at the time. |
start_local, end_local |
Naive local-time 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 a pandas DataFrame with the activity data.
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)
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 a pandas DataFrame with a timezone-aware datetime index.
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 "files=@activity1.fit" \
-F "files=@activity2.fit"
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"}