Timezones¶
Every timestamp in SweatStack is stored as an absolute instant in UTC, paired with the local UTC offset that was in effect where the data was recorded. Reads give you both representations. Writes require you to supply the offset. There is no separate "account timezone" and no IANA zone name anywhere in the API. The offset travels with each record.
This keeps two questions cleanly separated:
- When did it happen, on one global clock? Use the UTC instant. It sorts and compares correctly across athletes and across travel.
- What did the clock on the wall say? Use the local companion. That is what you show a user and what you bucket by calendar day.
The two representations¶
Datetime fields come in pairs. The absolute instant and its local wall-clock companion:
| Field | Meaning | Format |
|---|---|---|
start, end, timestamp |
Absolute UTC instant | ISO 8601 with a Z suffix, e.g. 2026-05-18T15:32:00Z |
start_local, end_local, timestamp_local |
Naive local wall-clock | ISO 8601 with no offset, e.g. 2026-05-18T17:32:00 |
The local companion is always present and never null. It is the instant with the record's offset already applied, so you never do timezone math to display it.
{
"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"
}
This activity was recorded at +02:00. The UTC instant is 15:32:00Z, the athlete's clock read 17:32:00. The difference between the two is the offset.
Reading timestamps¶
Use start (and end, timestamp) whenever you need a correct absolute ordering: sorting activities, comparing against a deadline, storing a canonical instant, merging timeseries from different sources.
Use start_local (and end_local, timestamp_local) for anything a human sees, and for grouping by calendar day. "Did I train today?" and "how much did I ride this week?" are local-calendar questions. Answer them with the local fields, not by converting UTC yourself.
start and end are timezone-aware UTC datetime objects. start_local and end_local are naive datetime objects holding the local wall-clock.
import sweatstack
activity = sweatstack.get_latest_activity()
activity.start # 2026-05-18 15:32:00+00:00 (aware, UTC)
activity.start_local # 2026-05-18 17:32:00 (naive, local wall-clock)
In a DataFrame from get_activities(as_dataframe=True), start and end are a single datetime64[ns, UTC] column, and start_local / end_local are naive-local columns. Bucket by local day on the local column:
activities = sweatstack.get_activities(as_dataframe=True)
# 7-day rolling distance, keyed on the local day each activity happened
activities.rolling("7d", on="start_local")["summary.distance"].sum()
curl -X GET "https://app.sweatstack.no/api/v1/activities/latest" \
-H "Authorization: Bearer {your_access_token}"
The response carries both start (UTC, Z) and start_local (naive local).
Writing timestamps¶
When you create or update a Trace, a Test, or a Workout, the timestamp you send must be offset-aware. A naive datetime (no offset) is rejected with 422 Unprocessable Entity. SweatStack stores the instant plus the offset you supplied, and returns both representations on subsequent reads.
Either an explicit offset or a Z suffix is accepted. Send the offset that matches where the session took place, so the stored local wall-clock is correct.
Attach a zone to the datetime. Use zoneinfo for a real location (it handles DST for you), or timezone.utc when the instant is already in UTC.
from datetime import datetime
from zoneinfo import ZoneInfo
import sweatstack
sweatstack.create_test(
sport="cycling.road",
start=datetime(2026, 5, 6, 10, 0, tzinfo=ZoneInfo("Europe/Amsterdam")),
)
A naive datetime(2026, 5, 6, 10, 0) 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 '{
"sport": "cycling.road",
"start": "2026-05-06T10:00:00+02:00"
}'
"2026-05-06T10:00:00" (no offset) returns 422.
Timeseries data¶
The timeseries DataFrames (both get_activity_data for one activity and get_longitudinal_data across many) are indexed by a timezone-aware UTC timestamp, so samples from activities in different timezones share one absolute axis and merge and sort correctly. A timestamp_local column carries the naive local wall-clock for each sample, derived from that sample's own activity offset, so it stays correct across DST boundaries and travel within a single query.
The rule of thumb:
- Merge and sort on the UTC index.
- Group and bucket by local calendar day on
timestamp_local.
from datetime import date, timedelta
import sweatstack
data = sweatstack.get_longitudinal_data(
sports=["cycling.road"],
start=date.today() - timedelta(days=90),
)
# Total work per local training day
data.groupby(data["timestamp_local"].dt.date)["power"].sum()
Date-range filtering¶
The list endpoints (GET /activities/, /tests/, /traces/) filter by date range on each record's own local date. A range like start=2026-06-01&end=2026-06-30 returns exactly the records whose local start falls in that month, regardless of where the athlete was. Pass dates in YYYY-MM-DD form.
curl -X GET "https://app.sweatstack.no/api/v1/activities/?start=2026-06-01&end=2026-06-30" \
-H "Authorization: Bearer {your_access_token}"
The longitudinal-data endpoint takes start and end as dates as well.
Why no timezone name¶
SweatStack models timezones as offsets, not IANA zone names (Europe/Amsterdam), because the upstream sources (device files, integrations) reliably provide an offset and do not reliably provide a zone. An offset is enough to reconstruct the local wall-clock, which is what the *_local fields give you. If you need a named zone for display, resolve it on your side from the athlete's known location; the API does not guess one for you.