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
| Type | Emitted when | |
|---|---|---|
ping | You sent a test delivery from the console | Live |
activity.created | A finished workout landed from a provider | 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 |
daily.summary | Overnight sleep, HRV and resting heart rate | Planned |
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
{
"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:
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. 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.updatedcan arrive before theactivity.createdit updates. - Delivery is at-least-once.
webhook-idis 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.summarywill 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.