Rate limits
#Rate limits
| Endpoint | Limit | Partitioned by |
|---|---|---|
POST /api/v1/oauth/token |
20 requests / minute | per client_id and per caller IP (whichever is hit first) |
every other /api/v1/* call |
60 requests / minute by default (may be raised per club) | per client only — never per IP |
Exceeding either is 429 RATE_LIMIT_EXCEEDED, with Retry-After: 60 on both. Retry-After is always an
integer number of seconds (never an HTTP date). There is no burst allowance beyond the stated rate.
{ "type": "https://sailracing.club/docs/errors#RATE_LIMIT_EXCEEDED", "status": 429,
"code": "RATE_LIMIT_EXCEEDED", "traceId": "…" }A separate case, 503 API_UNAVAILABLE (the read model didn't answer — nothing
to do with your call rate), carries a shorter Retry-After: 30 — same integer-seconds format, different
number because it's a different, usually brief problem. Both 429 and 503 are handled the same way: wait
exactly Retry-After seconds, then retry.
#Staying under the limit
You almost never need 60 calls/min for one club's data — results are cached for up to an hour at our end
(Caching & freshness), so polling faster than your Cache-Control: max-age just spends
your rate-limit budget on 304s (which still count against the limit). A sensible integration:
- Poll each endpoint you use once, on your own schedule (e.g. every 5–15 minutes), not per page view.
- Serve your own visitors from your own cache/database, not from a live call per request.
- If you operate many clients (an agency serving several clubs), give each club its own credential rather than fanning one client's limit out across all of them.
#What backs off automatically
The token you hold does not need refreshing more than once an hour, so a correct refresh-before-expiry implementation naturally stays far under the token endpoint's 20/min limit even under load.