Stridee
Stridee Docs

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.

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

FieldTypeDescription
external_user_idstring

Narrow to exactly one of your users, by your id for them. The way back from an id in your database to ours.

limitinteger · int64

Defaults to 50, capped at 200.

offsetinteger · int64

Responses

200Your users, most recently active first
FieldTypeDescription
accountsRequiredAccountView[]
totalRequiredinteger · int64

How many match the filter, so a client knows whether to ask for another page. Not the number in accounts.

EndUsersPage
{
  "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

FieldTypeDescription
idRequiredstring · uuid

Our id for the user, as returned by POST /v1/connect

Responses

200The user
FieldTypeDescription
connectionsRequiredConnectionView[]

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_atRequiredstring · date-time
eventsRequiredEventView[]

Their recent events, newest first — the same rows the stream page shows, narrowed to this person.

external_user_idRequiredstring
idRequiredstring · uuid
AccountDetail
{
  "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"
}
404No such user on this account

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

FieldTypeDescription
idRequiredstring · uuid

Our id for the user, as returned by POST /v1/connect

Request body

FieldTypeDescription
external_user_idRequiredstring

What you want to call this person from now on.

RenameEndUserRequest · application/json
{
  "external_user_id": "user_4821"
}

Responses

200Renamed
FieldTypeDescription
created_atRequiredstring · date-time
external_user_idRequiredstring

Yours, as it now stands.

idRequiredstring · uuid

Ours. Unchanged by a rename — that is the point of one.

updated_atRequiredstring · date-time
AccountRef
{
  "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"
}
400Empty or over-long `external_user_id`
403This account's users cannot be renamed
404No such user on this account
409Another of your users already has that 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

FieldTypeDescription
idRequiredstring · uuid

Our id for the user, as returned by POST /v1/connect

Responses

204Forgotten
403This account's users cannot be deleted
404No such user on this account

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

FieldTypeDescription
idRequiredstring · uuid

The activity, as data.id on activity.created

Responses

302Redirect to a short-lived download URL
404No such activity on your account, or it has no file

Start a link.

Request body

FieldTypeDescription
external_user_idRequiredstring

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.

providerRequiredstring

coros, polar, wahoo or garmin. A provider this deployment has no credentials for is a 400, not a 404 — the path exists, the argument does not.

return_uristring

Where to send their browser when consent is finished, with status and user_id appended as query parameters. status is success, denied or error.

Must be registered on your account first (POST /platform/redirect-uris). Omit it and your user lands on a plain page we host, which tells them the connection succeeded and nothing else — register a URI if you would rather they end up back in your product.

ConnectRequest · application/json
{
  "external_user_id": "user_4821",
  "provider": "coros",
  "return_uri": "https://app.yourapp.com/settings/devices"
}

Responses

200A URL to send the user to
FieldTypeDescription
connect_urlRequiredstring

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_atRequiredstring · date-time

When connect_url stops working. Mint a fresh one rather than caching this — it is a single-use URL, not a per-user constant.

user_idRequiredstring · uuid

Our id for the person you named. Stable across every call for the same external_user_id, and what connection rows and events refer to. You do not have to store it.

ConnectResponse
{
  "connect_url": "https://api.stridee.fit/connections/start?state=…",
  "expires_at": "2026-08-05T11:44:02Z",
  "user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}
400Unknown provider, bad user id, or an unregistered return URI

List connections.

Query parameters

FieldTypeDescription
external_user_idstring

Narrow to one of your users. Omit for every connection on the account.

Responses

200Live connections, newest first
FieldTypeDescription
connected_atRequiredstring · 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_idRequiredstring

The developer's own id for this person — echoed back so a list response is legible against their database without a second lookup.

idRequiredstring · uuid

What DELETE /v1/connections/{id} takes. Ours, and the id the platform's events name in connection_id.

providerRequiredstring

Which integration this authorization is at — coros, polar, wahoo or garmin.

statusRequiredstring

active | reauth_required | revoked.

reauth_required is the one to handle: the provider has stopped accepting our credentials, no more data will arrive, and the only fix is sending the athlete back through POST /v1/connect.

user_idRequiredstring · uuid

Our id for the person this authorization belongs to — what POST /v1/connect returned and what every event about them carries as user_id.

Here so that throwing it away stays as harmless as the docs promise. It is the id /v1/accounts/{id} takes, and without it on this response a developer who did not persist the connect response would have no way back to it. Safe to expose beside external_user_id: it is minted per tenant, so unlike provider_user_id it correlates nobody with anybody.

revoked_atstring · date-time

Always null on GET /v1/connections, which lists live connections only. The field is here because a revoked connection is still a connection this type can describe, and a shape that changed depending on the endpoint would be two types wearing one name.

scopestring

What the athlete consented to, as the provider worded it. Absent when the provider returned none — not every one of them says.

ConnectionView[]
[
  {
    "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"
  }
]

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

FieldTypeDescription
idRequiredstring · uuid

The connection to disconnect, as returned by GET /v1/connections

Responses

204Disconnected
404No such connection on this account

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

200The account this signature belongs to
FieldTypeDescription
account_idRequiredstring · uuid

The account the signature authenticated as.

Whoami
{
  "account_id": "0b6f2d14-59c3-4e7a-8d05-1af38b62c9e7"
}
401The signature was missing, stale, replayed or wrong

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