Errors
All errors follow this shape:
{
"error": "Human-readable description of what went wrong"
}
Status codes
| Status | Meaning |
|---|---|
400 | Bad request, invalid body, missing required field, or failed validation |
401 | Unauthorized, missing or invalid token |
403 | Forbidden, authenticated, but not allowed to perform this action |
404 | Not found |
409 | Conflict, e.g. duplicate slug or email |
429 | Too many requests, rate limit exceeded |
500 | Internal server error |
Tier-upgrade 403
When a user tries to fetch a premium deal they can’t see, you get a richer envelope so your UI can show an upgrade prompt. Alongside error you always get a stable, machine-readable reason (always tier_upgrade_required for this 403) plus the upgrade_required flag:
{
"error": "This deal requires a premium subscription",
"reason": "tier_upgrade_required",
"upgrade_required": true
}
Handling errors
Treat most of the 4xx range as a fixable client problem (bad input, expired token, missing record); retrying without changing the request returns the same error. The exception is 429, which is retriable: back off and retry after the delay in the Retry-After response header (the API sets it on rate-limited responses). Treat 5xx as transient, retry with exponential backoff up to ~3 attempts.
async function callApi(path, options) {
const res = await fetch(`https://perks.founderpass.com${path}`, options);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || `HTTP ${res.status}`);
}
return res.json();
}