Browse the docs

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 /v1 API 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, injectable fetch.
  • 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. zod is listed as a dependency only for the type declarations.

Install

bash
npm install @latido/sdk
# or: bun add @latido/sdk · pnpm add @latido/sdk

Quickstart

ts
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

ts
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

ts
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

ts
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)

bash
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   # biome

Types 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.