API reference
Every endpoint on the platform API, generated from the spec the API publishes about itself.
- Base URL
- https://api.stridee.fit
- Auth
- An Ed25519 signature over every request — there is no bearer token. How to sign.
- Spec
- /platform/openapi.json — OpenAPI 3.1, if you would rather generate a client than read this.
/v1/accounts
List your users.
Everyone you have ever named to us, including people who were sent a link and
never came back through it — those have no connections and appear nowhere in
GET /v1/connections, which is the difference between the two endpoints. One
row per person, most recently active first.
Query parameters
| Field | Type | Description |
|---|---|---|
| external_user_id | string | Narrow to exactly one of your users, by your id for them. The way back from an id in your database to ours. |
| limit | integer · int64 | Defaults to 50, capped at 200. |
| offset | integer · int64 |
Responses
| Field | Type | Description |
|---|---|---|
| accountsRequired | AccountView[] | |
| totalRequired | integer · int64 | How many match the filter, so a client knows whether to ask for another
page. Not the number in |
{
"accounts": [
{
"activities": 0,
"connected_at": null,
"created_at": "2026-08-05T11:44:02Z",
"external_user_id": "user_4821",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"last_event_at": null,
"providers": [
"string"
],
"status": "active"
}
],
"total": 0
}One user: their connections, and what has happened to them.
Connections here include revoked ones, unlike GET /v1/connections — "they
disconnected COROS on Tuesday" is the answer to the question that actually
gets asked.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | Our id for the user, as returned by |
Responses
| Field | Type | Description |
|---|---|---|
| connectionsRequired | ConnectionView[] | Every connection, including revoked ones. "They disconnected COROS on Tuesday" is the answer to the support question that actually gets asked, and a list that dropped the row would show an athlete who appears never to have connected anything. |
| created_atRequired | string · date-time | |
| eventsRequired | EventView[] | Their recent events, newest first — the same rows the stream page shows, narrowed to this person. |
| external_user_idRequired | string | |
| idRequired | string · uuid |
{
"connections": [
{
"connected_at": "2026-08-05T11:44:02Z",
"external_user_id": "user_4821",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"provider": "coros",
"revoked_at": null,
"scope": "workout",
"status": "active",
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}
],
"created_at": "2026-08-05T11:44:02Z",
"events": [
{
"created_at": "2026-08-05T11:44:02Z",
"data": "string",
"delivered": 0,
"deliveries": 0,
"end_user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"manual": true,
"provider": "coros",
"type": "string"
}
],
"external_user_id": "user_4821",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85"
}/v1/accounts/{id}
Change your id for one of your users.
For when your own user table was re-keyed and the id you linked somebody
under is not the id you have any more. Our user_id does not change, so no
connection moves, no activity grant is affected, no event is rewritten, and
nobody goes back through consent. The next POST /v1/connect with the new id
finds this person rather than creating a second one.
This is a rename, not a merge. If another of your users already goes by the id you send, you get a 409 and nothing changes — deciding what should happen to two sets of connections at the same provider is not a guess this API is willing to make on your behalf.
If what you actually want is to stop referring to someone at all, that is
DELETE /v1/accounts/{id}.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | Our id for the user, as returned by |
Request body
| Field | Type | Description |
|---|---|---|
| external_user_idRequired | string | What you want to call this person from now on. |
{
"external_user_id": "user_4821"
}Responses
| Field | Type | Description |
|---|---|---|
| created_atRequired | string · date-time | |
| external_user_idRequired | string | Yours, as it now stands. |
| idRequired | string · uuid | Ours. Unchanged by a rename — that is the point of one. |
| updated_atRequired | string · date-time |
{
"created_at": "2026-08-05T11:44:02Z",
"external_user_id": "user_4821",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"updated_at": "2026-08-05T11:44:02Z"
}/v1/accounts/{id}
Stop referring to one of your users.
This does not delete a person, and it cannot. What it deletes is your reference to them: your id for them, their authorizations through you, the consents behind those, any outstanding management link, their event history on your stream, and your permission to read the activities they shared with you. Their workouts are not yours and stay exactly where they are — as does every other developer's separate reference to the same athlete, which you could never see and still cannot.
Live connections are revoked first, so the athlete's grant at the provider is withdrawn on the way out under the usual rule: only if this was their last connection through Stridee, since the same person may use another product that also integrates with us.
You get one account.deleted event rather than an account.disconnected per
connection — a handler seeing four disconnections and then silence cannot
tell that from someone who merely unplugged everything. It carries the ids in
data and has no user_id of its own, because the user it would name no
longer exists.
Afterwards the id is free. POST /v1/connect with the same
external_user_id creates a new person with a new user_id, connected to
nothing, and none of the old history comes back.
Idempotent in the only sense that matters: deleting someone already deleted is a 404.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | Our id for the user, as returned by |
Responses
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
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | The activity, as |
Responses
/v1/connect
Start a link.
Request body
| Field | Type | Description |
|---|---|---|
| external_user_idRequired | string | Your id for this person, from your own database. The only identifier you have to send, and the only one you have to store. Call this twice for the same value and you get the same user back rather than a second one — which is what makes retrying safe. |
| providerRequired | string |
|
| return_uri | string | Where to send their browser when consent is finished, with Must be registered on your account first ( |
{
"external_user_id": "user_4821",
"provider": "coros",
"return_uri": "https://app.yourapp.com/settings/devices"
}Responses
| Field | Type | Description |
|---|---|---|
| connect_urlRequired | string | Send your user here. Redirect their browser — this is a consent screen a person has to read, not something to fetch server-side. It is a page we host, not the provider's authorize endpoint. It names you, names us, and carries our terms, because your user is about to authorize their watch data flowing through infrastructure they have never heard of and every provider agreement behind this API assumes they were told. One click later they are at the provider, and the rest of the flow is as it was. The name your user reads on it is the one on your Stridee account. Without one the page still works and simply names no product. |
| expires_atRequired | string · date-time | When |
| user_idRequired | string · uuid | Our id for the person you named. Stable across every call for the same
|
{
"connect_url": "https://api.stridee.fit/connections/start?state=…",
"expires_at": "2026-08-05T11:44:02Z",
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}List connections.
Query parameters
| Field | Type | Description |
|---|---|---|
| external_user_id | string | Narrow to one of your users. Omit for every connection on the account. |
Responses
| Field | Type | Description |
|---|---|---|
| connected_atRequired | string · date-time | When consent finished. Not when the row was created: a reconnection after a revoke moves this forward, because the question it answers is "since when have we had access", and the old answer stopped being true. |
| external_user_idRequired | string | The developer's own id for this person — echoed back so a list response is legible against their database without a second lookup. |
| idRequired | string · uuid | What |
| providerRequired | string | Which integration this authorization is at — |
| statusRequired | string |
|
| user_idRequired | string · uuid | Our id for the person this authorization belongs to — what Here so that throwing it away stays as harmless as the docs promise. It is
the id |
| revoked_at | string · date-time | Always null on |
| scope | string | What the athlete consented to, as the provider worded it. Absent when the provider returned none — not every one of them says. |
[
{
"connected_at": "2026-08-05T11:44:02Z",
"external_user_id": "user_4821",
"id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85",
"provider": "coros",
"revoked_at": null,
"scope": "workout",
"status": "active",
"user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}
]/v1/connections/{id}
Disconnect.
The connection stops working immediately and its tokens are dropped.
Your access ends here. The athlete's authorization at the provider is only withdrawn if this was their last connection through Stridee — the same person may use two products that both integrate with us, and revoking the grant on your behalf would take the other one down without either of you knowing.
Idempotent: disconnecting something already disconnected is a 404, not a second revocation.
Path parameters
| Field | Type | Description |
|---|---|---|
| idRequired | string · uuid | The connection to disconnect, as returned by |
Responses
Mint a link that lets one of your users manage their own connections.
The page it opens lists that person's live connections with you and lets them disconnect any of them. It shows nothing about any other developer they may also be connected through, and disconnecting here does not touch those.
Why this exists rather than just DELETE /v1/connections/{id}: the athlete
has no Stridee account and never will, so there is nothing to log them in as.
The link is the credential. If your product already has its own settings
screen, use the DELETE and skip this entirely.
Request body
| Field | Type | Description |
|---|---|---|
| external_user_idRequired | string | Your id for the person, the same value you passed to |
{
"external_user_id": "user_4821"
}Responses
| Field | Type | Description |
|---|---|---|
| expires_atRequired | string · date-time | |
| manage_urlRequired | string | Put this in front of your user — a settings screen, an email, a support reply. Anyone holding it can disconnect that person's watches from your product, so treat it like a password reset link rather than a page URL. |
{
"expires_at": "2026-08-05T11:44:02Z",
"manage_url": "https://api.stridee.fit/connections/manage?token=…"
}/v1/whoami
The endpoint a client is pointed at while it is being written.
It exists because the first signed request anybody makes is always wrong, and
the failures are hard to tell apart from outside: a clock, a stale key, a
@target-uri built with the wrong scheme, a body digested after a framework
re-encoded it. All four look like "401" against a real endpoint. Here the
only thing being tested is the signature, so the error is about the
signature — and a 200 means the client is correct, not that this particular
call happened to be.
It reads nothing and changes nothing, so it is also the one endpoint safe to leave in a customer's health check.
Responses
| Field | Type | Description |
|---|---|---|
| account_idRequired | string · uuid | The account the signature authenticated as. |
{
"account_id": "0b6f2d14-59c3-4e7a-8d05-1af38b62c9e7"
}Something wrong or missing on this page? Tell us in Discord.