Events
What the platform emits, what an event body contains, and why it contains so little.
An event says that something happened: a type, the user it concerns, and the ids and summary you need to act on it. It never carries the recording itself — that is one signed call away.
Event types
| Type | Emitted when | |
|---|---|---|
ping | You sent a test delivery — see Sending a ping | Live |
activity.created | A finished workout landed from a provider | Live |
wellness.created | A sleep, daily, HRV, stress or fitness summary landed — see Wellness | Live |
wellness.updated | The provider revised a summary you were already told about | Live |
account.connected | One of your users linked a provider | Live |
account.disconnected | That access ended — by your call, or by the user through their management link | Live |
account.reauth_required | The provider stopped accepting our credentials — your user has to reconnect | Live |
account.deleted | You dropped your reference to one of your users | Live |
activity.updated | Title, gear or laps changed after upload | Planned |
activity.deleted | The user removed it on the provider side | Planned |
workout.pushed | A structured session reached the watch | Planned |
workout.completed | A pushed session came back as done | Planned |
Planned types are designed but not emitted yet. Every endpoint receives every type, so
switch on type and ignore what you don't recognise.
What an event looks like
{
"id": "9f2c1e7a-4b83-4d21-9a6e-3c5f0d8b71a4",
"type": "activity.created",
"created": "2026-08-04T06:14:02Z",
"webhook_id": "2d7b45c1-8e0a-4f36-b512-9c7d3a6e04f8",
"nonce": "Kd4nWpLbEa9xTvRm2Cj7Lz0Bq2vNhCz7",
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6",
"provider": "coros",
"data": {
"object": "activity",
"id": "0f31a8c4-59d2-4e07-b6a1-8c74e2f95d30",
"provider_activity_id": "465061532017233920",
"sport": "run",
"start_time": "2026-08-04T06:12:00Z",
"device": "PACE 3",
"name": "Morning Run",
"file": {
"format": "fit",
"url": "https://api.stridee.com/v1/activities/0f31a8c4-59d2-4e07-b6a1-8c74e2f95d30/file"
}
}
}This is what a delivery opens to, never what is on the wire.
| Field | |
|---|---|
id | The event. The same event sent to three endpoints has one id. |
user_id | Your user, by the id /v1/connect returned. Absent on ping. |
provider | Which integration caused it. Absent on ping. |
webhook_id | Which of your endpoints this copy went to. |
nonce | Fresh per delivery — echo it back. |
data | The object's type, ids and summary. |
Getting the file
data.file.url on an activity.created is the recording itself — the FIT, TCX or GPX the
device wrote, unparsed. It is an ordinary signed request, so sign it the way you sign every
other call:
GET /v1/activities/0f31a8c4-59d2-4e07-b6a1-8c74e2f95d30/fileIt answers 302 with a short-lived URL that carries its own authorization. Follow the redirect; don't store what it points at, because it expires in minutes. The URL in the event body does not expire — keep that one and call it again whenever you need the bytes.
data.id is our id for this activity, and it is yours alone. Another developer whose user
is the same athlete gets a different id for the same workout.
file is null when there is nothing to fetch — Wahoo, for instance, won't share a
recording that started in a third-party app. The workout still happened; don't retry it.
Access is granted when an activity arrives, so it covers exactly the time your user was connected to you. Disconnecting stops new activities but does not take back ones already shared — their links keep working and they stay listed below.
Listing what you were sent
Deliveries can be missed, and nothing retries them. GET /v1/activities is how you catch
up: every activity you have been granted, newest first, in the same shape the event carried.
GET /v1/activities?since=2026-08-12T00:00:00Z&until=2026-08-13T00:00:00Z&limit=50{
"activities": [
{
"id": "7e14a9c3-2b60-4f85-9d3a-6c081ef47b52",
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6",
"external_user_id": "01JQ8F3M2KX7A9VBRC4T6NDZWE",
"provider": "garmin",
"provider_activity_id": "19284736501",
"sport": "running",
"name": "Threshold intervals",
"device": "Forerunner 965",
"start_time": "2026-08-13T06:14:00Z",
"received_at": "2026-08-13T07:02:11Z",
"file": {
"format": "fit",
"size": 412880,
"url": "https://api.stridee.com/v1/activities/7e14a9c3-2b60-4f85-9d3a-6c081ef47b52/file"
}
}
],
"has_more": true,
"next_starting_after": "7e14a9c3-2b60-4f85-9d3a-6c081ef47b52",
"total": 1284
}A row is an activity.created flattened: user_id and provider are the envelope's,
everything from provider_activity_id down is the data block, and file.url is the same
download URL. Hand one to the code that already handles a delivery.
Page against received_at, not start_time. received_at is when we granted you the
activity — within a second of when your endpoint was called — and it is the only axis that
moves forward. Providers backfill, so a ride from March can arrive today, and a job paging
by start time would step straight over it.
| Parameter | |
|---|---|
user_id | One athlete, by our id for them. GET /v1/accounts?external_user_id=… is the way back from yours. |
provider | Any provider on Provider support. An unknown one is an empty page, not an error. |
since | Granted at or after this RFC 3339 instant. |
until | Granted strictly before it — half-open, so one window's until is the next one's since and nothing is counted twice. |
starting_after | The id of the last activity on the previous page. See Paging. |
limit | Defaults to 50, capped at 200. |
A nightly reconcile is since=<last run>&until=<this run>, walked with starting_after
while has_more is true — then dedupe on id against what your handler already stored.
Pass both ends of the window and the set you are walking cannot change underneath you.
Ordering and duplicates
- Order is not guaranteed. Providers backfill and correct, so a workout from March can arrive today.
- The same delivery can arrive twice — a resend reuses its
webhook-id. Dedupe on it, and still echo the nonce. - An event id is not a delivery id. One event sent to three endpoints is one
idand threewebhook-ids. Both are UUID v4 with no prefix; store them as strings and never parse one.
Treat an event as a signal to reconcile, not a transaction to apply, and none of these matter.
Something wrong or missing on this page? Tell us in Discord. Need something the API doesn’t do yet? Request it on the roadmap.