Stridee
Stridee Platform Docs

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

TypeEmitted when
pingYou sent a test delivery — see Sending a pingLive
activity.createdA finished workout landed from a providerLive
activity.updatedIts name or sport changed after it landed — see Edits after uploadLive
wellness.createdA sleep, daily, HRV, stress or fitness summary landed — see WellnessLive
wellness.updatedThe provider revised a summary you were already told aboutLive
account.connectedOne of your users linked a providerLive
account.disconnectedThat access ended — by your call, or by the user through their management linkLive
account.reauth_requiredThe provider stopped accepting our credentials — your user has to reconnectLive
account.deletedYou dropped your reference to one of your usersLive
activity.deletedThe user removed it on the provider sidePlanned
workout.pushedA structured session reached the watchPlanned
workout.completedA pushed session came back as donePlanned

Planned types are designed but not emitted yet. An endpoint receives every type unless you choose which events it gets. Either way, switch on type and ignore what you don't recognise.

What an event looks like

This is what a delivery opens to, never what is on the wire.

data.workout_id is there when the provider can say which workout the athlete did this from — the id POST /v1/workouts returned. Apple can, for a session started from a workout you pushed to an Apple Watch. On every other provider it is absent, which means unknown rather than no.

Field
idThe event. The same event sent to three endpoints has one id.
user_idYour user, by the id /v1/connect returned. Absent on ping.
providerWhich integration caused it. Absent on ping.
webhook_idWhich of your endpoints this copy went to.
nonceFresh per delivery — echo it back.
dataThe object's type, ids and summary.
activity.created
{
  "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"
    }
  }
}

Getting the file

data.file.url on an activity.created is the recording itself — the FIT, TCX or GPX the device wrote. WHOOP is the exception: it exports no file, so the FIT there is one we write from WHOOP's summary of the workout, with no GPS track or samples in it. Fetch it with an ordinary signed request:

get /v1/activities/{id}/file
Reference

Download the raw file for an activity.

The data.file.url on an activity.created points here. It is an ordinary signed request — sign it exactly the way you sign every other call — and it answers 302 with a short-lived URL that needs no credential of its own. Follow the redirect; do not store what it points at. That URL expires in minutes, while the one in the event body does not, so keep the event's and call this again whenever you need the bytes.

What you get is what the device wrote: the provider's own FIT, TCX or GPX, unmodified and unparsed. data.file.format on the event tells you which, and the redirect target carries the matching content type.

Access is the grant that was recorded when the activity arrived, so this answers for exactly the workouts your user was connected to you for. Disconnecting stops new activities; it does not take away files from the period they had agreed to share. An activity whose provider offered no downloadable file — "file": null on the event — has nothing to serve and answers 404.

Path parameters
idstring · uuidRequired

The activity, as data.id on activity.created

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.

GET /v1/activities/{id}/file
GET /v1/activities/7e14a9c3-2b60-4f85-9d3a-6c081ef47b52/file HTTP/1.1
Host: api.stridee.com
Signature-Input: sig1=(…);keyid="…";alg="ed25519"
Signature: sig1=:…:
Response · 302
HTTP/1.1 302 Found

Edits after upload

An athlete who renames a workout on the provider's side — a ride retitled in Garmin Connect after it synced — produces an activity.updated. Its data is the same shape as the activity.created you already had, with the same id, describing the activity as it is now. Overwrite what you stored rather than merging fields.

It is sent only when the name or the sport actually changed, and only to you if you were already sent the activity.created. Garmin tells us about edits made in Garmin Connect, and WHOOP re-sends a workout when it changes. Those two are where you will see it. Any other provider produces it only if it happens to re-send a workout under a new name, and most never do.

Listing what you were sent

Deliveries can be missed, and nothing retries them. This is how you catch up: every activity you have been granted, in the same shape the event carried. A row is an activity.created flattened — user_id and provider are the envelope's, the rest is the data block — so hand one to the code that already handles a delivery.

get /v1/activities
Reference

List the activities you have been granted.

Every workout one of your users was connected to you for, newest first — the same rows that were announced to your endpoint as activity.created, in the same shape. This is the reconciliation path: if a delivery was missed, this is how you find it, and file.url is the same download URL the event carried.

Page against received_at, not start_time. Providers backfill and correct, so a workout from March can arrive today; the order here is the order you were told, which is the only one that is monotonic. since and until window that same axis, half-open, so a nightly job can ask for since=<last run> and get exactly what it has not seen.

Paged with a cursor: send starting_after = the id of the last row you received, and keep going while has_more. next_starting_after is that id, so a loop never has to reach into the array. A cursor names a row rather than a distance from one, so activities arriving while you page cannot shift the boundary under you — which an offset would, and did.

Access is the grant that was recorded when each activity arrived — the same rule GET /v1/activities/{id}/file applies. Disconnecting stops new activities; it does not remove the ones from the period your user had agreed to share, so they stay listed here. An activity whose provider offered no downloadable file has "file": null, and so does one whose bytes have not landed yet.

Query parameters
user_idstring · uuid

Narrow to one athlete, by our id for them — what POST /v1/connect returned and what events carry as user_id. GET /v1/accounts is the way back from your own id to this one.

providerstring

coros | polar | wahoo | garmin | zepp | hammerhead | fitbit | suunto | whoop. An unknown provider is an empty page, not a 400.

sincestring

Only activities granted at or after this RFC 3339 instant.

untilstring

Only activities granted strictly before it. Half-open, so one window's until is the next one's since and nothing is counted twice.

starting_afterstring · uuid

The id of the last activity on the previous page. Omit for the first one.

limitinteger · int64

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.

GET /v1/activities
GET /v1/activities HTTP/1.1
Host: api.stridee.com
Signature-Input: sig1=(…);keyid="…";alg="ed25519"
Signature: sig1=:…:
Response · 200
{
  "activities": [
    {
      "device": null,
      "external_user_id": "user_4821",
      "file": null,
      "id": "7e14a9c3-2b60-4f85-9d3a-6c081ef47b52",
      "name": null,
      "provider": "coros",
      "provider_activity_id": "string",
      "received_at": "2026-08-05T11:44:02Z",
      "sport": null,
      "start_time": null,
      "user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6",
      "workout_id": null
    }
  ],
  "has_more": false,
  "next_starting_after": null,
  "total": 1
}

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 id and three webhook-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.