Skip to content

Errors

Every non-2xx response from the Lucerna API uses the same JSON shape — whether you called the Waitlist API yourself or an SDK surfaced it through onError:

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

StatuscodeMeaning
400invalid_requestThe request body or path parameter failed validation. message names the offending field.
401unauthorizedThe API key is missing, invalid, or revoked. See Authentication.
403forbiddenThe key lacks the required grant, or the waitlist rejects this signup by policy.
404not_foundThe waitlist, gate, or invite code doesn't exist.
410expired / revokedThe invite code is no longer redeemable. (accept only)
429rate_limitedToo many requests — slow down and retry with backoff.
500internal_errorSomething went wrong on our side. Safe to retry.

SDKs handle this for you

The Gates and Identity SDKs never throw an API error into your code. Reads fail open (flags false, switches alive, variants null), transient failures are retried with backoff, and anything worth knowing is reported through the onError callback you pass at construction. The one exception: ready() rejects on 401/403 so a bad key fails loudly at startup.

The rest of this page is for direct HTTP callers.

Handling errors

ts
const res = await fetch(url, options);

if (!res.ok) {
  const { code, message } = await res.json();

  switch (code) {
    case "unauthorized":
    case "forbidden":
      throw new Error("Check LUCERNA_SERVER_KEY and its grants");
    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 write endpoints are idempotent by design, so 429 and 500 responses are safe to retry with backoff:

  • Capturing signups is idempotent by email — retries never create duplicates (already-present emails count as skipped).
  • Accepting an invite returns 200 again for an already-accepted code.
  • Identity syncs and Gates exposure events are deduplicated server-side.

Lucerna Developer Docs