Stridee
Stridee Platform Docs

Connect a device

Link one of your users to their watch, without holding a provider client secret or implementing a callback.

Your user has a Garmin watch and you want their workouts. Built yourself, that is a partner application, weeks of approval, a client secret, a PKCE verifier and an OAuth callback — per provider, each with its own quirks.

Here it is three calls, and none of them involve a provider credential. The registration is ours: one client per provider, one callback URL, one consent screen. What you get back is your user, linked.

Who's who

Three parties, and keeping them apart is most of understanding this page.

YouThe developer. Your Stridee account, your keys, your webhook endpoints.
Your userAn athlete who signed up for your product. They have no Stridee account and will never be asked for one.
The connectionOne of your users' authorization at one provider.

You refer to your users by your own id — whatever your database already calls them. That is the only identifier you send and the only one you have to store. We mint a user_id and hand it back, but nothing breaks if you throw it away: it is on every connection you list, and GET /v1/accounts looks it up from your id.

1. Register a return URI

Where your user's browser should land once they have finished consenting. Add it under Return URIs in the console before your first connect call.

It is matched as a prefix, so register the stem once and vary the rest per user:

HTTPS, no wildcards, and it has to name a host — we won't send your users to a URL you haven't claimed. Skip this step and your users finish on a plain page we host that says the connection worked: fine for a first try, not for production.

Register as many as you need — one per environment, and one per app. They are an allowlist, not a default: nothing is used unless you pass it as return_uri on the call below.

Text
https://app.yourapp.com/settings/devices

Mobile apps

Register your app's own scheme, in reverse-domain form, alongside your web URL:

Open connect_url in ASWebAuthenticationSession on iOS or a Custom Tab on Android, and pass this as return_uri from your mobile flow. When consent is done we redirect to it, the browser sheet closes, and your app reads status and user_id from the URL.

The scheme has to be lowercase, contain at least one dot, and be followed by a path. Single-word schemes like myapp:// are refused: any other app on the phone can claim one. The redirect carries only status and user_id — no code or token — so it is safe to deliver to a custom scheme.

Text
com.yourapp.ios:/stridee-callback

Call this from your backend with your id for the user, then redirect their browser to the connect_url it returns.

post /v1/connect
Reference

Start a link.

Returns a connect_url to send one of your users to. They read a consent page we host, approve at the provider, and land back on your return_uri; the account.connected event is what tells your backend it worked. You hold no provider credentials and implement no OAuth callback.

connect_url stops working after 30 minutes, so mint one per attempt rather than storing it. Retrying is safe: the same external_user_id always resolves to the same user_id.

Body parameters
external_user_idstringRequired

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.

providerstringRequired

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

zepp, hammerhead, fitbit, suunto and whoop are the ones that may answer that way on a healthy deployment. Zepp's credentials come from a partner agreement with Huami rather than a self-serve signup, so an API that has not been issued any cannot start the flow and says so. Hammerhead's are self-serve but new enough that not every deployment has been given them yet, and Fitbit's Suunto's and WHOOP's likewise.

langstring

The language of the pages we host for this user: the consent screen, and the confirmation after it when you pass no return_uri.

One of en, de, da, nb, sv, pl, cs, sk, es, zh, zh-Hant, ko or ja. Region subtags are ignored, so your user's device locale can be passed as it is — de-AT is German, es_MX is Spanish, and no is read as nb. Chinese is the exception: zh-Hant, zh-TW, zh-HK and zh-MO are Traditional, and every other zh tag is Simplified.

Omit it and the pages follow your user's browser language, falling back to English. Pass it when you know better than their browser does — it always wins. A language we have not translated yet is treated as omitted rather than a 400, so passing every user's locale is safe today and picks up new languages as they ship.

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). A mobile app can pass its own scheme, com.example.app:/callback, so the browser sheet it opened connect_url in closes back into the app. 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.

connect_url is a page we host, showing your logo beside ours and carrying our terms; one click later your user is at the provider. That click is required and there is no setting to skip it: your user is about to send their heart rate, sleep and GPS traces through infrastructure they have never heard of, and every provider agreement behind this API assumes they were told who receives it. Set the name and logo they see under Branding, and the colours and corner shape of every page we host for them — it previews the real pages as you type, on a phone and on a desktop.

provider is any of the nine on Provider support & limits that connect here. Everything below is the same whichever you name — the differences in how each provider handles tokens and consent are ours to carry. Apple Health is the one that doesn't: it has no consent page, and apple here is a 400. Your iOS app connects it instead — see Apple Health.

POST /v1/connect
POST /v1/connect HTTP/1.1
Host: api.stridee.com
Content-Type: application/json
Content-Digest: sha-256=:…:
Signature-Input: sig1=(…);keyid="…";alg="ed25519"
Signature: sig1=:…:

{
  "external_user_id": "user_4821",
  "lang": "de",
  "provider": "coros",
  "return_uri": "https://app.yourapp.com/settings/devices"
}
Response · 200
{
  "connect_url": "https://api.stridee.com/connections/start?state=…",
  "expires_at": "2026-08-05T11:44:02Z",
  "user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6"
}

Language

The pages we host — this one, the confirmation after it, and the manage page — are written in English, German, Danish, Norwegian, Swedish, Polish, Czech, Slovak, Spanish, Chinese (Simplified and Traditional), Korean and Japanese. Without lang they follow your user's browser language. Pass lang when your app knows better: someone using your product in German on a phone set to English should read German, and lang always wins.

Your user's locale can go in as it is. de-AT is German, es_MX is Spanish, no is read as nb, and a language we don't have yet is treated as if you had passed nothing. Chinese is the one language where the rest of the tag matters: zh-Hant, zh-TW, zh-HK and zh-MO are Traditional, and every other zh tag is Simplified.

3. Your user comes back

They approve, and we send them to your return_uri with two parameters appended:

status
successConnected. The tokens are stored and data will start arriving.
deniedThey declined at the provider. Nothing was linked and nothing was shared.
errorSomething went wrong on our side or the provider's. Nothing was saved.

Handle all three — denied is a normal outcome, not a failure.

Don't treat this redirect as the source of truth. It tells you where to send the browser next; the account.connected event is what tells your backend the connection exists. A user who closes the tab mid-redirect still connected.

Text
https://app.yourapp.com/settings/devices?status=success&user_id=4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6

4. Get the event

With stridee listen running, this arrives on your local handler the moment they finish:

user_id is the value /v1/connect returned, which is how you map it back to your own user. From then on their workouts arrive as activity.created — see Events for the shape and Getting the file for the recording.

account.connected
{
  "id": "9f2c1e7a-4b83-4d21-9a6e-3c5f0d8b71a4",
  "type": "account.connected",
  "created": "2026-08-05T11:16:44Z",
  "webhook_id": "2d7b45c1-8e0a-4f36-b512-9c7d3a6e04f8",
  "nonce": "Kd4nWpLbEa9xTvRm2Cj7Lz0Bq2vNhCz7",
  "user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6",
  "provider": "coros",
  "data": {
    "connection_id": "a2f81b60-3c47-49d5-b8e2-70a4c9f13d85"
  }
}

Linking leaves you with two lists. Connections are one per authorization at one provider, so a user with two watches is two rows. Users are one per person, including people you sent a link to who never finished it. Everything you can do with either is in the API reference:

ToCall
List who is connectedGET /v1/connections
Find a user by your id, or list everyoneGET /v1/accounts
Disconnect one providerDELETE /v1/connections/{id}
Let your user manage their own connectionsPOST /v1/connections/manage-link
Change your id for someonePATCH /v1/accounts/{id}
Forget a user and everything you hold about themDELETE /v1/accounts/{id}

Three things no single endpoint says:

  • reauth_required means reconnect, not forget. The provider stopped accepting our credentials. The account.reauth_required event is your cue, and the only notice anyone gets: start a new link for the same external_user_id, which resolves to the same user.
  • A second provider is the same call. Call /v1/connect again with another provider and the same external_user_id: one user, two connections. Your ids are opaque to us, so user_4821 and 4821 are two different people — pick one form and keep it.
  • The provider's own id for your user is never returned. Two developers serving the same person could otherwise join their user tables on it.

Something wrong or missing on this page? Tell us in Discord. Need something the API doesn’t do yet? Request it on the roadmap.