Browse the docs

API reference

Errors and limits

The error envelope

Every error the API raises on purpose has the same JSON shape (errorResponseSchema):

json
{
  "errorCode": "PLAN_LIMIT_REACHED",
  "message": "The free plan allows 10 connectedMembers",
  "details": { "limit": "connectedMembers", "tier": "free", "max": 10 }
}
  • errorCode is a LatidoErrorCode (exported by @latido/common and @latido/sdk). Branch on it, not on message, which is for humans and may change.
  • details appears only on some errors (today: PLAN_LIMIT_REACHED).
  • Two responses do not carry an errorCode: an unknown route (404 {"message":"Not Found"}) and an unhandled server failure (500 {"message":"Internal Server Error"}). The SDK raises LatidoApiError with code: undefined for them.
  • No request-id header is sent today; LatidoApiError.requestId is read from Latido-Request-Id / X-Request-Id / Request-Id for when one is.

With the SDK, every non-2xx becomes a LatidoApiError (status, code, message, details, retryAfterSeconds, body); see the SDK reference for the other error classes.

Error codes of the public API

HTTP errorCode When
400 INVALID_ARGUMENT_ERROR The body is not JSON; a request the domain refuses (e.g. ingest of a family the store does not carry, a provider that does not upload through ingest).
400 REDIRECT_URL_NOT_ALLOWED The connect session's redirectUrl origin is not an allowed origin of the project.
400 PROVIDER_APP_NOT_ALLOWED A provider that is not connectable in this mode or not set up for the project.
400 PROVIDER_APP_CREDENTIALS_MISSING A custom provider app without client id and secret.
400 PROVIDER_NOT_OFFERED Hosted page: a provider outside the session.
400 PLANNED_WORKOUTS_NOT_SUPPORTED The provider does not accept planned workouts (only Garmin, Suunto, COROS, Wahoo and the demo provider do).
401 UNAUTHORIZED Missing, unknown or revoked credential; the wrong kind of credential for the surface. No WWW-Authenticate header.
403 FORBIDDEN Authenticated but not allowed: e.g. an ingest upload with a test key.
403 PLAN_LIMIT_REACHED A resource limit of the plan (see below).
404 MEMBER_NOT_FOUND, CONNECTION_NOT_FOUND, HEALTH_RECORD_NOT_FOUND, PLANNED_WORKOUT_NOT_FOUND, PROJECT_NOT_FOUND, PROVIDER_APP_NOT_FOUND, ENTITY_NOT_FOUND Unknown id — or one that belongs to another project or mode (they look the same from outside).
409 CONNECTION_NOT_ACTIVE The connection is not active (e.g. pushing a planned workout through an expired one).
409 CONFLICT The request collides with the current state.
410 CONNECT_SESSION_EXPIRED Hosted page: the connect link is unknown or older than an hour.
422 UNPROCESSABLE_CONTENT The body or query failed schema validation; message lists the zod issues.
423 ENTITY_LOCK_CONTENTION_ERROR Another request held the same entity's lock too long. Safe to retry (the SDK does).
429 PLAN_LIMIT_REACHED (details.limit: "apiRequestsPerMonth") The account's monthly request quota is spent.
429 RESYNC_RATE_LIMITED A resync of this connection ran less than 5 minutes ago (the message says how long to wait).
500 UNEXPECTED_ERROR A bug or an unavailable dependency.
503 TOKEN_CIPHER_NOT_CONFIGURED_ERROR The server cannot encrypt or decrypt provider credentials right now. Retry later; contact support if it persists.

The full enum, including codes only the dashboard and hosted flow use, is exported by the SDK as LatidoErrorCode.

Plan limits

Plan limits apply the same way to REST and MCP.

Limit Free Lifetime 1 Lifetime 2 Lifetime 3 Lifetime 4
Projects 1 2 5 15 50
Connected members (live) 10 100 500 2,000 10,000
API requests / month 10,000 100,000 500,000 2,000,000 10,000,000
Webhook endpoints 1 3 10 25 100
Team seats 1 2 5 15 50
Own provider apps (BYO keys) yes yes yes yes yes
MCP yes yes yes yes yes
Planned workouts no yes yes yes yes
Data retention 30 days 365 days 730 days 1,825 days 3,650 days

Lifetime tiers come from AppSumo licenses (one license, one tier). They never include Latido's managed apps for providers that charge or gate partners (Garmin, Oura, WHOOP, Dexcom, Google Health): those work with your own keys on every tier.

  • Connected members counts live members with at least one active connection; test members never count. A member already connected does not count twice. Reaching the limit fails the connect callback with reason=plan_limit_reached and an ingest upload with 403 PLAN_LIMIT_REACHED.
  • PLAN_LIMIT_REACHED carries details: { limit, tier, max }; limit is a PlanLimitName (projects, connectedMembers, apiRequestsPerMonth, webhookEndpoints, teamSeats, customProviderApps, mcp, plannedWorkouts); for yes/no features max is false.
  • The dashboard shows usage (GET /dashboard/usage) and the plan (GET /dashboard/billing).

Rate limits

  • Monthly quota. Every authenticated /v1 and /mcp request counts (a Redis counter per account and calendar month, UTC). The one over the quota is refused with 429 PLAN_LIMIT_REACHED and not counted. No Retry-After is sent: waiting a few seconds does not help, so the SDK does not retry it.
  • Resync. POST /v1/connections/{id}/sync runs at most once per connection every 5 minutes (429 RESYNC_RATE_LIMITED, not retried by the SDK).
  • No per-second limit is enforced by the API today. If a proxy in front of the API ever adds one, its 429s (with or without Retry-After) are retried by the SDK.
  • Timeouts. Every route answers within 60 seconds.

Request limits

What Limit
Page size (limit) 1-200, default 50 (MCP tools: 1-100, default 25)
Ingest upload 1-1000 records per call
Member metadata 2 KB of JSON
externalUserId 1-200 characters
Connect session providers 1-20
Connect session lifetime 1 hour
Resync days 1-365 (default 7, capped by the provider's maxBackfillDays)
Planned workout name ≤ 100 chars, 1-100 steps, repeat blocks ≤ 50 steps × 100 repetitions
Webhook endpoint eventTypes ≤ 50
Webhook response 2xx within 10 s

Retrying safely

  • GET and DELETE are safe to repeat. So are POST /v1/members (an upsert by externalUserId), POST /v1/connect/sessions (a repeat only makes another link), POST /v1/connections/{id}/sync (a repeat is refused with RESYNC_RATE_LIMITED) and ingest (idempotent by providerRecordId).
  • POST /v1/members/{id}/planned-workouts is not: a repeat pushes a second workout. Retry it only after 423 / 429, which mean nothing was done.
  • 423 and 429 (except the two persistent 429s above) mean the request was refused before any work. 5xx and timeouts mean it may or may not have happened.

The SDK applies exactly these rules (exponential backoff with jitter, Retry-After honoured up to maxRetryDelayMs).