Syncing many customers
A reliable polling loop for every connected customer: per-customer checkpoints, shared catalogs, rate limits, and the errors that should pause a customer.
The API has no webhooks and no "changed since" filter, so integrations poll. This page sets up a polling loop for a platform with many customers.
One job per customer
Treat each customer key as an independent sync with its own state:
| Store per customer | Why |
|---|---|
| The integration key, encrypted | Its only credential. |
The organization id, from GET /organization | To link Vitruve data to your customer account. |
A checkpoint: the largest completedAt synced | Where the next run starts. |
| Status: active, paused (plan) or disconnected (key revoked) | To skip customers that cannot answer. |
The rate limit is 30 requests per minute per customer key, 403 answers included, so one large customer cannot slow down the others. Run customers in parallel with a modest concurrency, and keep each customer's own requests sequential.
The loop
For each active customer:
GET /workouts?from=<checkpoint>&limit=100, paging withcursorwhilemeta.hasMoreistrue. Completed workouts are filtered and sorted bycompletedAt, newest first.- Skip workouts whose
idyou already stored.fromis inclusive, so the workout at the checkpoint comes back on every run. GET /workouts/{id}for each new workout to get its sets and repetitions.- When every page succeeded, store the new largest
completedAtas the checkpoint.
To pick up coaches' corrections to recent workouts, also re-read a short window on each run, for example the last 7 days, and upsert by id. Edits do not change completedAt, and deleted workouts simply stop being returned.
Catalogs
Metrics, units, prescription types and muscles are global: the same for every customer. Load them once with any key and cache them for a day. Exercises combine Vitruve's global library with each customer's own exercises: cache the global ones (source=global) once, and each customer's (source=organization) per customer.
Errors that change a customer's status
| Answer | Action |
|---|---|
401 Invalid API key | Mark the customer disconnected. Stop calling the key: repeated 401s use up your secret's failed-authentication budget, and past it every customer answers 429 until the minute ends. |
401 Organization is not active | Mark the customer disconnected and remove the key. |
403 Your plan no longer includes API access | Mark the customer paused and retry once a day. |
429 Rate limit of 30 requests per minute exceeded | Wait Retry-After seconds for that customer, then continue from where you stopped. |
Some answers are about your platform, not the customer: every customer would answer the same. Stop the whole sync instead of marking customers, and fix the cause first.
| Answer | Cause |
|---|---|
401 Partner credentials required or Invalid partner credentials | Your secret is missing, wrong or revoked. |
403 Request origin not allowed for this integration | Your egress address changed or is not registered. |
403 This integration is not available | Your platform's integration is disabled. Contact Vitruve. |
429 Too many failed authentication attempts | Your secret, or your address, spent its failed-authentication budget. Wait Retry-After seconds. |
503 Rate limiter unavailable | Retry with exponential backoff. |
5xx | Retry with exponential backoff. Do not move the checkpoint. |
The full list is in Error codes. Keep the X-Request-Id of a failed request: it lets Vitruve support trace it.
How often
Hourly polling suits most integrations. A poll with nothing new costs one request per customer. An initial backfill is the expensive part: each workout detail is one request, so 10,000 workouts take at least 34 minutes for that customer. Spread backfills over time and run them separately from incremental syncs.