Tokens
Three token types are returned by /v1/oauth/token. They serve different jobs.
Access token
Purpose: authorize a single API call. Send it as Authorization: Bearer <token> on every request to a Loop service.
Format: opaque, prefixed with lph_at_. Not a JWT — services validate by calling identity’s introspection endpoint or by checking the token against a fast cache (Redis). This means we can revoke a token instantly; we don’t have to wait for the JWT to expire.
Lifetime: 1 hour by default. Configurable per-app down to 5 minutes for high-sensitivity integrations.
Cardinality: unlimited per (user, client). The same user can have many access tokens active for the same app.
Refresh token
Purpose: obtain a new access token without sending the user through the consent flow again.
Format: opaque, prefixed with lph_rt_.
Lifetime: 90 days from last use. Each refresh extends the window.
Rotation: every successful refresh issues a new refresh token AND invalidates the previous one. If you ever see the same refresh token used twice, that’s a leak — Loop will revoke the entire grant for the user × client and alert the user.
Cardinality: one active refresh token per (user, client). Re-running the authorization flow replaces it.
ID token
Purpose: OIDC — prove who the user is to your app.
Format: JWT, signed by Loop’s identity key. Public keys available at https://identity.platform.loop.health/.well-known/jwks.json.
Lifetime: 5 minutes. Don’t store; use it once to identify the user, then rely on the access token + introspection for ongoing calls.
Claims:
| Claim | Meaning |
|---|---|
sub | the user’s stable Loop ID. Never reused, never recycled. |
iss | always https://identity.platform.loop.health |
aud | your client_id |
email | only if email scope was granted |
email_verified | always true if email is present |
name | only if profile scope was granted |
brand | the brand context for this session (loop, loopbio, …) |
iat, exp, nonce | standard |
Introspection
Services use POST /v1/oauth/introspect to check that an access token is still valid:
POST https://identity.platform.loop.health/v1/oauth/introspect
Authorization: Basic <base64(client_id:client_secret)>
token=lph_at_...Response:
{
"active": true,
"sub": "user_01HXY...",
"client_id": "client_01HXY...",
"scope": "read:biomarkers read:protocols",
"exp": 1735689600,
"brand": "loop"
}If active: false, the service returns 401 to the original caller. Don’t cache introspection responses beyond exp.
Revocation
A user can revoke a grant from their Connected Apps page. Apps can revoke their own tokens by calling:
POST https://identity.platform.loop.health/v1/oauth/revoke
Authorization: Basic <base64(client_id:client_secret)>
token=<access_or_refresh_token>
&token_type_hint=access_token # or refresh_tokenA 200 means “this token is no longer valid” — even if it was already invalid. No further confirmation needed.
Revoking a refresh token revokes all associated access tokens. Revoking an access token only revokes that one.
What tokens are never used for
- Server-to-server inside the platform. Use
client_credentials(M2M) for that. Seeservices/identityM2M docs. - Long-term machine integrations without a user. Same — M2M.
- Webhook signature verification. Webhooks use a separate signing secret per app, not the user’s tokens.