Skip to content

OAuth2

Info

For local development, you can generate an API key instead, or authenticate with the Python SDK.

In a hurry, or familiar with OAuth2? The short version:
  1. Create an application at app.sweatstack.no/applications/new.
  2. Client ID. The Application ID shown after saving is your OAuth2 client ID.
  3. Scopes. Request any mix of data:read, data:write, and profile. Add offline_access if you use PKCE and want a refresh token.
  4. Authorization endpoint. https://app.sweatstack.no/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&scope={space_separated_scopes}&response_type=code
  5. Token endpoint. Exchange codes and refresh tokens at POST https://app.sweatstack.no/api/v1/oauth/token.
  6. Authenticated calls. Send the access token on every request: Authorization: Bearer {access_token}.

Integrating SweatStack as one more platform? Four things that differ from other platforms

If you have integrated other providers, these are the places where an expectation that is correct elsewhere is wrong here:

  1. PKCE is optional, not required. The plain Authorization Code flow with a client secret is fully supported. Use PKCE if your client can. You are not forced to.
  2. Credentials are self-service. Nobody emails you a client ID or secret. You get them from the app's settings page.
  3. The client secret is not created automatically. Create it yourself in the Secrets section. You need no secret at all if you use PKCE.
  4. There is no approval queue. Filling in the four required fields makes your app public the moment you save. Nothing to submit, nobody to wait for.

New here? The quick reference has every endpoint, scope, and credential on one page.

Why OAuth2?

OAuth2 solves two distinct problems:

  1. Delegated access. A user can let your app call our API without sharing their password.
  2. Scoped tokens. The access you obtain is limited in time (it expires) and in scope (for example, only data:read instead of data:write).

The core idea is an exchange in which your application, you (the developer), and the end user's browser pass short-lived codes and long-lived tokens between them, so each party sees only what it needs.

The OAuth2 specification defines several flows. SweatStack supports two. Which one you choose depends on your application type.

Flow Best for
Authorization Code Server-side apps
Authorization Code + PKCE Browser-based apps and mobile apps

If you are unsure, pick Authorization Code + PKCE. It's the modern default, it is secure, and it works almost everywhere.

1. Create an application

  1. Sign up or sign in if you haven't already. Any user can create applications.
  2. In the menu bar at the top, go to Settings → API → Create Application, or go there directly.
  3. Fill in the fields:
    • Name: shown on the consent screen.
    • Description: shown on the consent screen.
    • URL: the URL of your application, where users can find out more about it.
    • Image URL: the URL of an image, shown on the consent screen.
    • Redirect URIs: where we send the user back. Accepts https, loopback http (localhost or 127.0.0.1, any port), and private-use custom schemes for native apps (for example com.example.app://oauth/callback or myapp://callback). https and loopback match by sub-path; custom schemes match exactly. Public plain http:// and URIs with a #fragment are rejected. Full list: accepted formats.
    • Privacy Statement/Policy URL: the URL of your privacy statement or policy.
  4. Click Create Application.
  5. The Application ID shown at the top is your OAuth2 client ID.

Your new app is private

Only your own SweatStack account can complete its OAuth flow until you fill in the four required fields. Anyone else sees a "still private" page that says so. See Going live for what makes an app public and how to let other people connect.

Optional. If you use the Authorization Code flow without PKCE, you need a client secret:

  1. In the Secrets section at the bottom, enter a label for the secret and click Create new secret.
  2. Copy the secret and store it in a secure location. The Secret is your OAuth2 client secret. You can create as many secrets as you want and revoke them at any time.

Warning

Treat the secret like a password. Never put it in mobile or SPA code, or in any other place that users can reach.

2. Authorize the user

Determine the scopes your app needs

SweatStack supports these scopes:

  • data:read: read access to all user data: activities, dailies (including health measures the user entered), and tests. There is no narrower read scope. Under the terms, your app is the independent controller of the data it receives.
  • data:write: write access to user data, including deleting data. Does not allow reading data.
  • profile: access to profile information, including the email address.
  • offline_access: issues a refresh token, so your app stays connected while the user is away. PKCE (public) clients receive a refresh token only if they request offline_access. Clients that authenticate with a client secret always receive one. If a mobile or single-page app finds refresh_token empty, this scope is almost always why.
  • openid: OpenID Connect. Every code exchange returns an id_token (an RS256 JWT carrying sub, name, given_name, family_name and your nonce), whether or not you request this scope. Request it anyway, so OIDC client libraries behave. The ID token carries no email. The email is on userinfo, with the profile scope.

Separate multiple scopes with spaces, as defined by RFC 6749 §3.3: scope=data:read data:write (URL-encoded, the space becomes %20 or +). Commas are also accepted for backwards compatibility, but spaces are canonical and we can drop comma support in the future.

Write-only integrations

If your app only pushes data into SweatStack (an uploader that never reads data back), request data:write alone. It is sufficient on its own and, deliberately, does not grant read access, so users see you asking for the minimum. Upload targets: activities and dailies.

Build the authorization URL

With PKCE, you generate a code verifier and a code challenge:

  1. Generate a random code verifier of 43 to 128 characters.
  2. Create the code challenge by hashing the verifier with SHA-256 and base64url-encoding the result.

Store the code verifier securely. You need it later, when you exchange the authorization code for tokens.

GET https://app.sweatstack.no/oauth/authorize
    ?response_type=code
    &client_id={client_id}
    &redirect_uri={redirect_uri}
    &scope={space_separated_scopes}
    &state={optional_state}
    &code_challenge={code_challenge}
    &code_challenge_method=S256

A complete authorization URL looks like this:

https://app.sweatstack.no/oauth/authorize?response_type=code&client_id=abcd1234&redirect_uri=https://your-awesome-app.com/oauth-callback&scope=data:read&code_challenge=1234567890&code_challenge_method=S256

GET https://app.sweatstack.no/oauth/authorize
    ?response_type=code
    &client_id={client_id}
    &redirect_uri={redirect_uri}
    &scope={space_separated_scopes}
    &state={optional_state}

A complete authorization URL looks like this:

https://app.sweatstack.no/oauth/authorize?response_type=code&client_id=abcd1234&redirect_uri=https://your-awesome-app.com/oauth-callback&scope=data:read

Now redirect the user to that URL, or open it in a popup. They sign in, grant access, and SweatStack redirects them back to you.

Two optional parameters change what the user sees on the way. OpenID Connect's prompt=none skips the consent screen for a user who has already granted your app these scopes (with one deviation from the specification, noted on the quick reference), and SweatStack's own email_auth_mode=existing_only steers new users to a wearable sign-in. Every parameter the endpoint accepts is on the quick reference.

3. Receive the callback and exchange for tokens

The redirect looks like this:

https://your-awesome-app.com/oauth-callback?code={authorization_code}&state={optional_state}

Exchange the code, together with the code_verifier you generated earlier, for tokens:

POST https://app.sweatstack.no/api/v1/oauth/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=authorization_code" \
    -d "code={authorization_code}" \
    -d "client_id={client_id}" \
    -d "code_verifier={code_verifier}"

The redirect looks like this:

https://your-awesome-app.com/oauth-callback?code={authorization_code}&state={optional_state}

Exchange the code, together with the client secret, for tokens:

POST https://app.sweatstack.no/api/v1/oauth/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id={client_id}" \
    -d "client_secret={client_secret}" \
    -d "grant_type=authorization_code" \
    -d "code={authorization_code}" \

The response looks like this:

{
  "access_token": "eyJhbGciOi…",
  "expires_in": 900,
  "refresh_token": "def50200…",
  "scope": "data:read",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOi…"
}

Store the access_token and the refresh_token in a secure location. The id_token is the OpenID Connect identity token. You can ignore it unless your library consumes one.

4. Refresh the access token

An access token expires after 15 minutes. If the access token has expired and the user has not withdrawn consent, exchange the refresh token for a new access token.

POST https://app.sweatstack.no/api/v1/oauth/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=refresh_token" \
    -d "refresh_token={refresh_token}" \
    -d "client_id={client_id}" \
POST https://app.sweatstack.no/api/v1/oauth/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id={client_id}" \
    -d "client_secret={client_secret}" \
    -d "grant_type=refresh_token" \
    -d "refresh_token={refresh_token}" \

SweatStack returns a new access token, in the same shape as the first token response:

{
  "access_token": "eyJhbGciOi…",
  "expires_in": 900,
  "refresh_token": "def50200…",
  "scope": "data:read",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOi…"
}

5. Authenticate API requests

Send the access token as a Bearer authorization header on every API request:

GET https://app.sweatstack.no/api/v1/activities/ \
     -H "Authorization: Bearer {access_token}"

Token lifecycle

Token Lifetime Notes
Access token 15 minutes Refresh it on a 401 response, or before it expires.
Refresh token No expiry Invalidated only when the user revokes your app's permissions.

Access tokens are JWTs, signed with RS256, and carry the standard exp claim (the expiry as a Unix timestamp). You can read the expiry without a network call by decoding the token. The token response also includes the standard expires_in field (seconds until expiry), for clients that prefer not to parse the JWT.

Note

If you decode the access token, the granted scopes are in a scopes claim (a JSON array), not in the RFC 9068 space-delimited scope string. Most integrations should read the scopes from the scope field of the token response (space-delimited), not from the JWT.

An app that is used seasonally or sporadically (a user opens it once a month) never has to send the user through OAuth again, as long as the refresh token is still valid. Treat a 401 as "refresh and retry", not as "send the user back through OAuth".

OAuth2/OpenID client libraries

You can implement OAuth2 by hand, but for most use cases a client library is the better path.

Discovery

If your client library supports OpenID Connect discovery, point it at:

https://app.sweatstack.no/.well-known/openid-configuration

The library fetches the configuration and configures every endpoint, supported scope, and token-signing algorithm itself. You don't hard-code authorization or token URLs.

The discovery document looks like this:

{
    "issuer": "https://app.sweatstack.no",
    "authorization_endpoint": "https://app.sweatstack.no/oauth/authorize",
    "token_endpoint": "https://app.sweatstack.no/api/v1/oauth/token",
    "userinfo_endpoint": "https://app.sweatstack.no/api/v1/oauth/userinfo",
    "jwks_uri": "https://app.sweatstack.no/.well-known/jwks.json",
    "response_types_supported": ["code"],
    "subject_types_supported": ["public"],
    "id_token_signing_alg_values_supported": ["RS256"],
    "scopes_supported": ["openid", "profile", "offline_access", "data:read", "data:write"],
    "claims_supported": ["sub", "exp", "name", "given_name", "family_name"],
    "token_endpoint_auth_methods_supported": ["none", "client_secret_post", "client_secret_basic"]
}

For OAuth2-only clients (no OIDC), the equivalent metadata document is at /.well-known/oauth-authorization-server. It includes grant_types_supported and registration_endpoint instead of the OIDC-specific fields.

Verifying tokens

Tokens are JWTs signed with RS256. SweatStack serves the public keys at /.well-known/jwks.json, and the discovery document points to that URL. Standard JWT libraries that support JWKS rotate keys on their own.

The previous well-known paths under /api/v1/.well-known/... are deprecated. They still respond, for clients pinned to that location. New clients should use the root paths.

6. Check whether the user actually has data

After the token exchange, call userinfo and check the issue field. It is null when there is nothing to say. Otherwise show issue.message. If issue.destination is set, add a button that creates a Portal link for it when the user clicks. This is how you tell "this user never connected a device" apart from "my integration is broken" when GET /activities comes back empty.

See Why does this user have no data?.

Next steps

  • Python SDK: the Python SDK wraps the API, and Explore the API is the fastest path into it.
  • The Portal: when a user needs to connect a source or grant a permission, send them to the Portal instead of building those screens yourself. It carries your branding and returns them to you.
  • API reference: every endpoint, parameter, and response schema is in the API reference. To run a request live against your account, use the API playground.