Skip to main content

Errors

Every error is JSON with the same shape:

{"error": "A short human-readable message."}

The message is intentionally generic and is not a stable API — do not branch on its text. Branch on the status code.

All error responses carry Cache-Control: no-store, so a failure is never cached and a retry always reaches the origin.

The full list

CodeBodyCauseWhat to do
400Invalid discipline. Use 'strength' or 'running'.discipline was present but not one of the two valid values.Fix the caller. Retrying will not help.
401Unauthorized.Missing, malformed, or wrong X-Api-Key.Configuration error. Alert; do not retry.
405Method not allowed.Any method other than GET or OPTIONS, including HEAD.Fix the caller.
500Server error.Server-side failure — misconfiguration or a database error.Transient. Retry with backoff, and serve your cached copy meanwhile.

Handling advice

500 is the only one worth retrying. 400, 401, and 405 are all caller bugs, and retrying them produces the same result while adding load. Retry 500 with exponential backoff and a cap, and fall back to your last good cached response.

Never let an error empty your catalog. The realistic failure mode is that your integration renders an empty page during a brief outage. Keep the last successful response and serve it on failure — the catalog changes slowly enough that a day-old copy is far better than nothing.

async function getPlans() {
try {
const res = await fetch('https://kamee.fit/api/plans', {
headers: {'X-Api-Key': process.env.KAMEE_API_KEY},
});

if (res.status === 401) {
alertOps('Kamee API key rejected');
return lastGoodPlans();
}
if (!res.ok) {
return lastGoodPlans();
}

const {plans} = await res.json();
storeLastGood(plans);
return plans;
} catch {
return lastGoodPlans();
}
}

There is no rate limiting on this endpoint, so no error means "slow down". That is not licence to poll hard — see Caching and freshness for a sane interval.