Quickstart: Node.js / Express
This guide walks through a working Express app that implements Connect with Loop. By the end you will have a server that redirects users to the Loop consent screen and exchanges the authorization code for tokens.
Prerequisites: Node.js 20+, an OAuth client registered at developers.platform.loop.health (see the main quickstart).
Install dependencies
npm init -y
npm install expressEnvironment variables
export LOOP_CLIENT_ID="client_01HXY..."
export LOOP_CLIENT_SECRET="secret_..."
export LOOP_REDIRECT_URI="http://localhost:3000/callback"Full example — server.mjs
import crypto from "node:crypto";
import express from "express";
const app = express();
const AUTHORIZE_URL = "https://identity.platform.loop.health/v1/oauth/authorize";
const TOKEN_URL = "https://identity.platform.loop.health/v1/oauth/token";
const { LOOP_CLIENT_ID, LOOP_CLIENT_SECRET, LOOP_REDIRECT_URI } = process.env;
const sessions = new Map();
app.get("/login", (_req, res) => {
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
const state = crypto.randomBytes(16).toString("hex");
sessions.set(state, { verifier });
const params = new URLSearchParams({
response_type: "code",
client_id: LOOP_CLIENT_ID,
redirect_uri: LOOP_REDIRECT_URI,
scope: "openid profile read:biomarkers",
state,
code_challenge: challenge,
code_challenge_method: "S256",
});
res.redirect(`${AUTHORIZE_URL}?${params}`);
});
app.get("/callback", async (req, res) => {
const { code, state, error } = req.query;
if (error) return res.status(400).send(`Authorization failed: ${error}`);
if (!state || !sessions.has(state)) return res.status(400).send("Invalid state — possible CSRF");
const { verifier } = sessions.get(state);
sessions.delete(state);
const body = new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: LOOP_REDIRECT_URI,
client_id: LOOP_CLIENT_ID,
client_secret: LOOP_CLIENT_SECRET,
code_verifier: verifier,
});
const tokenRes = await fetch(TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
if (!tokenRes.ok) {
const err = await tokenRes.json();
return res.status(502).json({ error: "Token exchange failed", detail: err });
}
const tokens = await tokenRes.json();
// Make an authenticated API call
const biomarkers = await fetch(
"https://clinical.platform.loop.health/v1/biomarkers/me",
{ headers: { Authorization: `Bearer ${tokens.access_token}` } },
);
res.json({
message: "Connected to Loop!",
scopes: tokens.scope,
biomarkers: biomarkers.ok ? await biomarkers.json() : null,
});
});
app.listen(3000, () => process.stdout.write("Listening on http://localhost:3000\n"));Run it
node server.mjsOpen http://localhost:3000/login in a browser. You will be redirected to the Loop consent screen. After granting access, the callback endpoint exchanges the code and calls the biomarkers API.
Token refresh
When the access token expires (default: 1 hour), use the refresh token:
async function refreshTokens(refreshToken) {
const body = new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: LOOP_CLIENT_ID,
client_secret: LOOP_CLIENT_SECRET,
});
const res = await fetch(TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
const tokens = await res.json();
// IMPORTANT: always store the new refresh_token — the old one is invalidated
return tokens;
}Using the SDK instead
The @platform/sdk package handles PKCE, token refresh, and error retry automatically:
import { LoopClient } from "@platform/sdk";
const loop = new LoopClient({
clientId: process.env.LOOP_CLIENT_ID,
clientSecret: process.env.LOOP_CLIENT_SECRET,
redirectUri: process.env.LOOP_REDIRECT_URI,
});
// In your /callback handler:
const tokens = await loop.exchangeCode(req.query.code, req.query.state);
// Anywhere afterwards — SDK auto-refreshes on 401:
const biomarkers = await loop.clinical.listBiomarkers({ userId: tokens.userId });Next steps
- Authorization flow — understand the full state machine and error paths.
- Scopes — choose the right scopes for your integration.
- Security — PKCE details, redirect URI rules, what to never log.