Stridee
Stridee Docs

Events

What the platform emits, what an event body contains, and why it contains so little.

An event says that something happened, and gives you the ID to go and read it. It is not the data itself.

Event types

TypeEmitted when
pingYou sent a test delivery from the consoleLive
activity.createdA finished workout landed from a providerLive
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.updatedTitle, gear or laps changed after uploadPlanned
activity.deletedThe user removed it on the provider sidePlanned
workout.pushedA structured session reached the watchPlanned
workout.completedA pushed session came back as donePlanned
daily.summaryOvernight sleep, HRV and resting heart ratePlanned

The planned ones are designed and not yet emitted. Write your handler to switch on type and ignore what it doesn't recognise, and they'll cost you nothing when they land.

What an event looks like

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.fit/v1/activities/0f31a8c4-59d2-4e07-b6a1-8c74e2f95d30/file"
    }
  }
}

This is what a delivery's ciphertext opens to — never what lands on the wire. See Encrypted deliveries for the envelope around it.

Two of those fields belong to the delivery rather than to the thing that happened. nonce is fresh per endpoint, so the same event sent to three of them carries three different ones; your handler returns it to confirm the delivery — see Confirming a delivery. webhook_id names which of your endpoints this copy went to.

user_id is the subject: your user, by the id we returned when you linked them. Not a Stridee account — the athlete has none, which is the whole premise of Connecting a device. provider says which integration caused it.

ping is the one type you can cause on demand, and the only one with no subject at all — no user_id, no provider, because a person pressed a button and no integration was involved. Naming one would make a test delivery indistinguishable from traffic. See Sending a ping for the shape and what it proves.

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:

Shell
GET /v1/activities/0f31a8c4-59d2-4e07-b6a1-8c74e2f95d30/file

It 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. Providers don't always have one: Wahoo, for instance, won't share a recording that originated in a third-party app. The workout still happened and the event still describes it, so treat a null file as a fact about that activity rather than as an error, and don't retry it.

Access is the permission we recorded when the activity arrived. So it covers exactly the period your user was connected to you: if they disconnect for a month and come back, nothing from that month is yours. Disconnecting stops new activities — it does not take away files from the period they had agreed to share, so links you already hold keep working.

Why the event object is thin

A webhook URL is a public endpoint. Anyone who learns it can POST to it, and a handler that trusts what arrives is a handler that can be fed anything. Encryption does not close that on its own — anyone can seal a body to a public key, because that is what public means. That is why user_id is in the event: checking it against your own records is what closes the gap.

The data block carries an object type and the ids you need to act on it rather than the object itself, which is a rule about the API's event model and not about delivery. activity.created is where that rule pays: what it hands you is an id and a URL, and the recording behind them can be gigabytes of samples that most handlers never open. Deliveries are sealed to a key only you hold, so nothing in the path can read a body regardless of how much is in it — which is why a delivery may carry a provider's own summary inline and save you the read-back call. The thin form is what an event is; the sealed envelope is what makes putting more in it safe.

Ordering and duplicates

  • Order is not guaranteed. Providers backfill, retry and correct. An activity.updated can arrive before the activity.created it updates.
  • Delivery is at-least-once. webhook-id is stable across retries — dedupe on it, and still echo the nonce on the duplicate.
  • Absence is information. If a provider never sends resting heart rate, no daily.summary will invent one.

Treat the event as a signal to reconcile, not as a transaction to apply, and every one of these stops mattering.

Something wrong or missing on this page? Tell us in Discord.