API reference
@latido/sdk
TypeScript client for the Latido API — one API for Garmin, Strava, Polar, Suunto, COROS, Oura, WHOOP, Withings, Fitbit / Google Health, Wahoo, Dexcom, Huawei, Apple Health and Health Connect.
- Typed resources that mirror the public
/v1API one to one. - Every response is validated against Latido's published contract (zod), so what your code receives is exactly what the types say.
- Cursor pagination as async iterators.
- Retries with exponential backoff on 429 / 423 / 5xx / network errors (honouring
Retry-After), per-attempt timeouts, injectablefetch. - Webhook signature verification on Web Crypto: Node ≥ 20, Bun, Deno, Cloudflare Workers.
- No runtime dependencies: the contract schemas and a private copy of zod are bundled.
zodis listed as a dependency only for the type declarations.
Install
npm install @latido/sdk
# or: bun add @latido/sdk · pnpm add @latido/sdkQuickstart
import { Latido } from "@latido/sdk";
const latido = new Latido({ apiKey: process.env.LATIDO_API_KEY! }); // lat_test_… or lat_live_…
// 1. A hosted connect link for one of your users (valid one hour).
const { data: session } = await latido.connect.sessions.create({
externalUserId: "user-42",
redirectUrl: "http://localhost:3000/devices", // test keys accept localhost
});
console.log("Send the user to", session.url);
// 2. Back on your redirect URL: ?status=connected&provider=…&connectionId=…
const result = latido.connect.parseRedirect(request.url);
if (result.status === "connected") console.log("Connected", result.provider);
// 3. Read their data, page after page.
for await (const workout of latido.members.workouts.iterate(session.memberId, {
from: "2026-10-01",
to: "2026-10-27",
})) {
console.log(workout.sport, workout.distanceMeters, "m in", workout.durationSeconds, "s");
}The full walkthrough (test key → demo provider → data → webhook) is in
docs/api/quickstart.md.
Client options
new Latido({
apiKey: "lat_live_…", // required
baseUrl: "https://api.getlatido.com", // default
fetch: customFetch, // default: globalThis.fetch
timeoutMs: 30_000, // per attempt
maxRetries: 2, // extra attempts
retryBaseDelayMs: 500, // doubled per retry, with jitter
maxRetryDelayMs: 30_000, // a longer Retry-After is not waited for
});Every method takes a last options argument: { signal?: AbortSignal; timeoutMs?; maxRetries? }.
Resources
Every method returns the API's own envelope — { data }, { data, meta } — validated.
| SDK | HTTP |
|---|---|
latido.providers.list() |
GET /v1/providers |
latido.connect.sessions.create({ externalUserId, redirectUrl, providers?, metadata? }) |
POST /v1/connect/sessions |
latido.connect.parseRedirect(url) |
reads the hosted page's ?status=… |
latido.members.list({ externalUserId?, search?, limit?, cursor? }) / .iterate(…) |
GET /v1/members |
latido.members.create({ externalUserId, metadata? }) |
POST /v1/members (idempotent upsert) |
latido.members.get(memberId) |
GET /v1/members/{memberId} |
latido.members.delete(memberId) |
DELETE /v1/members/{memberId} (GDPR erasure) |
latido.members.connections(memberId) |
GET /v1/members/{memberId}/connections |
latido.members.workouts.list(memberId, query) / .iterate(…) |
GET /v1/members/{memberId}/workouts |
latido.members.sleepSessions |
…/sleep-sessions |
latido.members.dailySummaries |
…/daily-summaries |
latido.members.bodyMeasurements |
…/body-measurements |
latido.members.heartRate |
…/heart-rate |
latido.members.glucose |
…/glucose |
latido.records.get(recordId, { include? }) |
GET /v1/records/{recordId} |
latido.connections.get(connectionId) |
GET /v1/connections/{connectionId} |
latido.connections.sync(connectionId, { days? }) |
POST /v1/connections/{connectionId}/sync |
latido.connections.revoke(connectionId) |
DELETE /v1/connections/{connectionId} |
latido.plannedWorkouts.create(memberId, { provider, workout }) |
POST /v1/members/{memberId}/planned-workouts |
latido.plannedWorkouts.get(plannedWorkoutId) |
GET /v1/planned-workouts/{plannedWorkoutId} |
latido.ingest.upload(memberId, { provider, records }) |
POST /v1/members/{memberId}/ingest |
Record queries take from / to (ISO date or instant; from inclusive, to exclusive, on
the record's start), provider, include: "raw" (adds the provider's payload), limit
(1-200, default 50) and cursor. Lists are newest first.
Errors
import { LatidoApiError, LatidoErrorCode } from "@latido/sdk";
try {
await latido.members.get("mem_unknown");
} catch (error) {
if (error instanceof LatidoApiError && error.code === LatidoErrorCode.MEMBER_NOT_FOUND) {
// error.status === 404, error.details, error.requestId
}
throw error;
}| Class | When |
|---|---|
LatidoApiError |
non-2xx: status, code (LatidoErrorCode), message, details, requestId, retryAfterSeconds, body |
LatidoConnectionError |
no response (DNS, TLS, reset) |
LatidoTimeoutError |
no response within timeoutMs (subclass of LatidoConnectionError) |
LatidoResponseValidationError |
a 2xx whose body breaks the contract (the SDK is older than the API): issues, body |
LatidoWebhookVerificationError |
a webhook that is not genuine: reason |
All extend LatidoError.
Retries. Idempotent calls (every GET and DELETE, members upsert, connect sessions, sync,
ingest) are retried on 408 / 423 / 429 / 500 / 502 / 503 / 504 and network errors. Planned
workout pushes are retried only on 423 / 429 (refused before any work), never on 5xx, so a
workout is never sent twice. A 429 PLAN_LIMIT_REACHED (monthly quota spent) or
RESYNC_RATE_LIMITED (one resync per connection every 5 minutes) is never retried.
Webhooks
import { Latido, LatidoWebhookVerificationError } from "@latido/sdk";
// Any framework: pass the raw body, untouched.
export async function POST(request: Request) {
try {
const event = await Latido.webhooks.verify(
await request.text(),
request.headers.get("Latido-Signature"),
process.env.LATIDO_WEBHOOK_SECRET!, // whsec_…
{ toleranceSeconds: 300 },
);
if (event.type === "workout.created") {
// event.data.memberId, event.data.recordId, event.data.record…
}
return new Response(null, { status: 204 });
} catch (error) {
if (error instanceof LatidoWebhookVerificationError) {
return new Response(error.reason, { status: 400 });
}
throw error;
}
}verify is async (Web Crypto), compares in constant time, checks the timestamp tolerance
(default 300 s) and validates the event envelope. Latido.webhooks.sign(body, secret) builds
a valid header for your own tests. See docs/api/webhooks.md.
Types
Records, members, connections, inputs and enums are exported: WorkoutRecord,
SleepSessionRecord, DailySummaryRecord, BodyMeasurementRecord, HeartRateSampleRecord,
GlucoseSampleRecord, HealthRecord, Member, MemberDetail, Connection, Provider,
PlannedWorkout, LatidoWebhookEvent, ProviderSlug, HealthDataType, SportType,
ConnectionStatus, WebhookEventType, LatidoErrorCode, PlanLimitName…
Development (monorepo)
make -C packages/sdk test # bun test, fetch mocked, no network
make -C packages/sdk build # typecheck + dist/index.js + dist/types
make -C packages/sdk docs # regenerate docs/api/data-model.md tables
make -C packages/sdk check # biomeTypes and schemas come from @latido/common at build time (scripts/build.ts): the bundle
swaps @hono/zod-openapi for plain zod with a no-op .openapi(), and the declarations of the
common schemas are emitted under dist/types/common/ with relative imports, so the published
package needs nothing from the monorepo.