Skip to content
Developers
OpenAPI

Caching & freshness

API v1 Last updated 2026-09-30 View as Markdown

#Caching & freshness

Every /api/v1 data response is served from a server-side cache, refreshed at least once an hour (3600 seconds by default — a club can be configured for a shorter window on race days, never a longer one). Results can be up to that old. Four headers tell you exactly how old:

Header Meaning
Cache-Control: private, max-age=<n> seconds left in the current server-side cache window — cache the response yourself for at least this long
ETag: "<hash>" a strong hash of the exact body — send it back as If-None-Match next time
Last-Modified when the underlying data last changed (list responses: the newest item)
X-Data-As-Of ISO-8601 UTC timestamp (with milliseconds) of when this body was read — the "as of" time to show a viewer if you display it

#Conditional requests

# first call
curl -s -D- https://sail-club-server.cloud.run/api/v1/events/EVENT_ID \
  -H "Authorization: Bearer $TOKEN" -o event.json | grep -i etag
# ETag: "3f9a1c0d2b7e4a6f8c1d2e3f9a1c0d2"

# later — send it back
curl -s -o /dev/null -w '%{http_code}\n' https://sail-club-server.cloud.run/api/v1/events/EVENT_ID \
  -H "Authorization: Bearer $TOKEN" \
  -H 'If-None-Match: "3f9a1c0d2b7e4a6f8c1d2e3f9a1c0d2"'
# 304 — your cached copy is still correct, no body sent

A matching If-None-Match returns 304 with no body — cheaper for both sides than re-downloading unchanged JSON. It still counts as a call against your rate limit, the same as a 200 would — a conditional request saves bandwidth, not rate-limit budget. See Rate limits.

#How fresh a change actually is

A change made in the panel (a race scored, a result corrected) reaches /api/v1 after: the internal read model rebuilds (roughly 30 seconds, with a safety re-check at 5 minutes) plus whatever remains of the current cache window (up to the full hour). There is no way to force an immediate refresh from the API side — if a club needs sub-hour freshness during a regatta, that's a cache-window change on our side, not something an integration can request per-call.

Don't call /api/v1 live per visitor request. A simple, correct pattern:

on your own schedule (e.g. every 15 min, well under max-age):
    call the endpoint with If-None-Match: <your stored ETag>
    if 304: keep serving your existing cached copy, done
    if 200: replace your cached copy and stored ETag with the new body
serve every visitor from your own cache/database, never from a live call

This also means a temporary outage on our side (503 API_UNAVAILABLE) never has to be visible to your visitors — you already have the last good copy.