Pagination, errors and limits
Pagination
Every list endpoint takes limit (1–100, default 50; sessions allow up to 200) and offset (default 0) and returns:
"pagination": { "total": 128, "limit": 50, "offset": 0, "has_more": true }
Page with offset += limit while has_more is true. total is the count across all pages for the filters you sent. Lists have a fixed order (classes by name, sessions by date, seasons newest first), so pages are stable between calls unless the studio changes something in between.
Errors
Every non-2xx response is:
{ "error": { "code": "invalid_request", "message": "limit must be between 1 and 100" } }
Branch on code. message is for humans and logs and may change.
| Status | code | When |
|---|---|---|
| 400 | invalid_request | A parameter is missing, malformed or not allowed (limit=0, status=draft, category_id=abc) |
| 401 | unauthorized | No X-API-Key, unknown key, or revoked key |
| 403 | forbidden | The key lacks the scope this endpoint needs |
| 404 | not_found | No such record for this studio (another studio's id is a 404, not a 403), or no such endpoint |
| 405 | method_not_allowed | Anything other than GET or OPTIONS. The API is read-only |
| 429 | rate_limited | Too many requests from your IP. Wait and retry |
| 500 | internal_error | Our fault. Retry with backoff; tell us if it persists |
Every response under /api/v1/, including unknown paths and write attempts, is JSON in this envelope, so await res.json() is always safe.
Unknown query parameters are ignored. Known parameters with bad values are rejected, never silently defaulted, so a typo shows up in development rather than as wrong data in production.
Null means blank
Optional text (description, address, and so on) is null when the studio left it blank, never an empty string. Optional objects (category, location, instructor) are null when unset, never {}.
Rate limit
300 requests per minute per IP address, across all endpoints. Over that, requests get 429 until the minute rolls over; there is no Retry-After header, so wait a minute. For a nightly sync or a schedule page this is far more than enough. If you are polling, poll slowly: the catalog changes a few times a week, not a few times a minute.
Caching
The API never caches between callers. Every response carries Cache-Control: private, no-store, so a CDN or shared proxy will not serve one key's data to another. Cache on your side if you call often: a few minutes is plenty for a catalog.
Versioning
The path carries the version (/api/v1/). Within v1 we add fields and endpoints but never remove or rename them, change a type, or change a status code. A breaking change would ship as /api/v2/ with v1 kept running. New fields can appear at any time, so parse what you need and ignore the rest.