Errors
Every non-2xx response uses the same JSON shape:
json
{
"code": "invalid_request",
"message": "entries: Array must contain at least 1 element(s)"
}code— a stable, machine-readable string. Branch on this.message— a human-readable explanation. It may change; don't parse it.
Check the HTTP status code first, then code for the specifics.
Status codes
| Status | code | Meaning |
|---|---|---|
400 | invalid_request | The request body or path parameter failed validation. message names the offending field. |
401 | unauthorized | The server key is missing or invalid. (signup only) |
403 | forbidden | The waitlist is closed or rejects this signup (e.g. a blocked email). |
404 | not_found | The waitlist or invite code doesn't exist. |
410 | expired / revoked | The invite code is no longer redeemable. (accept only) |
429 | rate_limited | Too many requests — slow down and retry with backoff. |
500 | internal_error | Something went wrong on our side. Safe to retry. |
Handling errors
ts
const res = await fetch(url, options);
if (!res.ok) {
const { code, message } = await res.json();
switch (code) {
case "unauthorized":
throw new Error("Check LUCERNA_SERVER_KEY");
case "invalid_request":
throw new Error(`Bad request: ${message}`);
case "rate_limited":
case "internal_error":
// Transient — retry with exponential backoff.
break;
default:
throw new Error(`${code}: ${message}`);
}
}Retrying safely
The signup endpoint is idempotent by email — retrying a request never creates duplicate entries (already-present emails are counted as skipped). Accepting an invite is idempotent too. That makes 429 and 500 responses safe to retry with backoff.