API reference
Errors and limits
The error envelope
Every error the API raises on purpose has the same JSON shape (errorResponseSchema):
{
"errorCode": "PLAN_LIMIT_REACHED",
"message": "The free plan allows 10 connectedMembers",
"details": { "limit": "connectedMembers", "tier": "free", "max": 10 }
}errorCodeis aLatidoErrorCode(exported by@latido/commonand@latido/sdk). Branch on it, not onmessage, which is for humans and may change.detailsappears 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 raisesLatidoApiErrorwithcode: undefinedfor them. - No request-id header is sent today;
LatidoApiError.requestIdis read fromLatido-Request-Id/X-Request-Id/Request-Idfor 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_reachedand an ingest upload with403 PLAN_LIMIT_REACHED. PLAN_LIMIT_REACHEDcarriesdetails: { limit, tier, max };limitis aPlanLimitName(projects,connectedMembers,apiRequestsPerMonth,webhookEndpoints,teamSeats,customProviderApps,mcp,plannedWorkouts); for yes/no featuresmaxisfalse.- The dashboard shows usage (
GET /dashboard/usage) and the plan (GET /dashboard/billing).
Rate limits
- Monthly quota. Every authenticated
/v1and/mcprequest counts (a Redis counter per account and calendar month, UTC). The one over the quota is refused with429 PLAN_LIMIT_REACHEDand not counted. NoRetry-Afteris sent: waiting a few seconds does not help, so the SDK does not retry it. - Resync.
POST /v1/connections/{id}/syncruns 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 byexternalUserId),POST /v1/connect/sessions(a repeat only makes another link),POST /v1/connections/{id}/sync(a repeat is refused withRESYNC_RATE_LIMITED) and ingest (idempotent byproviderRecordId). POST /v1/members/{id}/planned-workoutsis 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).