Build a partner app
What this is: the complete path for a third-party developer building an app that integrates with Loop on behalf of users.
Who it’s for: developers at partner companies who got pointed at this site.
Time: 30 minutes to a working hello-world; 1–2 days to a polished integration.
The shape of a partner integration
- User clicks “Connect with Loop” in your app.
- Loop renders a consent screen listing the scopes you requested.
- User approves → your app gets an access token.
- Your app calls Loop services with the token.
- Loop pushes events to your webhook endpoint.
Step 1: Register your app
Sign in at https://developers.platform.loop.health and create an OAuth client:
| Field | Value |
|---|---|
| Name | ”Your app name” |
| Type | Web / Mobile / SPA |
| Redirect URIs | https://your-app.example.com/auth/loop/callback |
| Scopes | Pick from the scope reference |
| Webhook URL | https://your-app.example.com/webhooks/loop (optional) |
You get back:
client_id— public, embed in your appclient_secret— confidential (web apps only)webhook_signing_secret— to verify webhook deliveries
Step 2: Implement the OAuth flow
The authorization_code + PKCE flow. Six steps.
2a. Generate PKCE verifier + challenge (in your backend or session storage)
import crypto from "node:crypto";
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
// Store `verifier` in session keyed by the `state` you'll send.2b. Redirect user to /authorize
const url = new URL("https://identity.platform.loop.health/v1/oauth/authorize");
url.searchParams.set("response_type", "code");
url.searchParams.set("client_id", process.env.LOOP_CLIENT_ID!);
url.searchParams.set("redirect_uri", "https://your-app.example.com/auth/loop/callback");
url.searchParams.set("scope", "openid profile read:biomarkers");
url.searchParams.set("state", randomState);
url.searchParams.set("code_challenge", challenge);
url.searchParams.set("code_challenge_method", "S256");
res.redirect(url.toString());2c. Handle the callback
User returns to your /auth/loop/callback?code=<...>&state=<...>. Verify state matches what you stored; if not, drop (CSRF).
2d. Exchange code for tokens
const res = await fetch("https://identity.platform.loop.health/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: req.query.code,
redirect_uri: "https://your-app.example.com/auth/loop/callback",
client_id: process.env.LOOP_CLIENT_ID!,
client_secret: process.env.LOOP_CLIENT_SECRET!,
code_verifier: storedVerifier,
}),
});
const tokens = await res.json(); // { access_token, refresh_token, expires_in, id_token, scope }2e. Store tokens server-side keyed by your local user.
2f. When access_token expires (after 1 hour), refresh:
const res = await fetch("https://identity.platform.loop.health/v1/oauth/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: storedRefreshToken,
client_id: process.env.LOOP_CLIENT_ID!,
client_secret: process.env.LOOP_CLIENT_SECRET!,
}),
});
const newTokens = await res.json();
// Save the NEW refresh_token — old one is now dead.Step 3: Use the SDK (recommended)
Skip the raw HTTP and use @platform/sdk instead:
import { LoopClient } from "@platform/sdk";
const loop = new LoopClient({
clientId: process.env.LOOP_CLIENT_ID!,
clientSecret: process.env.LOOP_CLIENT_SECRET!,
redirectUri: "https://your-app.example.com/auth/loop/callback",
});
// In your /callback handler:
const tokens = await loop.exchangeCode({
code: req.query.code,
state: req.query.state,
codeVerifier: storedVerifier,
});
// Anywhere afterwards:
import { createClinicalClient } from "@platform/sdk-clinical";
import { platformHost, SERVICE_NAMES } from "@platform/hosts";
const clinical = createClinicalClient({
baseUrl: platformHost({ service: SERVICE_NAMES.CLINICAL }),
accessToken: tokens.access_token,
});
const biomarkers = await clinical.listBiomarkers({ userId: tokens.sub });SDK handles refresh, error normalization, retries, and idempotency keys.
Step 4: Receive webhooks
If you want push notifications for events (new biomarker, subscription change, etc.):
- Subscribe to the events you want in the developer portal.
- Implement your webhook endpoint:
import crypto from "node:crypto";
import { WEBHOOK_HEADERS, EVENT_NAMES } from "@platform/contracts";
app.post("/webhooks/loop", (req, res) => {
const sig = req.header(WEBHOOK_HEADERS.SIGNATURE);
const expected = crypto
.createHmac("sha256", process.env.LOOP_WEBHOOK_SECRET!)
.update(rawBody)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig!), Buffer.from(expected))) {
return res.status(401).end();
}
const eventId = req.header(WEBHOOK_HEADERS.EVENT_ID);
if (await alreadyProcessed(eventId)) return res.status(200).end();
const event = JSON.parse(rawBody);
switch (event.type) {
case EVENT_NAMES.CLINICAL_BIOMARKER_PARSED_V1:
await handleBiomarker(event.data);
break;
// ... more event types
}
await markProcessed(eventId);
res.status(200).end();
});Full details: Webhooks.
Step 5: Handle errors
Always plan for:
invalid_token(401) — refresh and retry once.insufficient_scope(403) — re-authorize asking for the missing scope.rate_limited(429) — honorRetry-After, back off with jitter.not_found(404) — the resource doesn’t exist OR isn’t visible to this user.
Full error reference: Errors.
Step 6: Apply for BAA (if you need PHI scopes)
Scopes like read:biomarkers, read:protocols, read:check_ins return PHI. You can’t complete the OAuth flow for these scopes until you’ve signed a BAA.
Apply at developers.platform.loop.health/baa. Admin review takes 1–3 business days.
Step 7: Go live
Before flipping production traffic:
- Webhook signature verification implemented + tested
- Token refresh implemented + tested
- Error handling covers all platform error codes
- Rate-limit backoff with jitter implemented
- Logging excludes tokens and PHI
- User has a way to disconnect (calls revoke endpoint)
- BAA signed if requesting PHI scopes
- Production redirect URI added to your app registration
Related
- Connect with Loop — the canonical OAuth spec
- Authorization flow
- Scopes
- Tokens
- Errors
- Webhooks
- Security
- Auth model