MostlyQR

Errors

The REST API uses standard HTTP status codes and returns a JSON error body:

json
{ "error": "unauthorized", "message": "Missing or invalid API key" }

# Status codes

Status error When
400 invalid-argument Malformed body or a missing required field.
401 unauthorized Missing, invalid, or revoked API key.
403 permission-denied Beyond your plan’s entitlement (a feature your plan doesn’t include).
404 not-found No link/campaign/job with that id, or not owned by your account.
405 method_not_allowed That HTTP method isn’t supported on the path.
409 failed-precondition The resource isn’t in a state that allows the operation — e.g. editing a frozen link, or minting serialized codes beyond your Enterprise allotment (the limit is in the message).
429 rate_limited / resource-exhausted Per-key rate limit hit (rate_limited — includes a retry_at timestamp and X-RateLimit-* response headers) or a plan quota exhausted (resource-exhausted). Back off and retry.
5xx internal Something went wrong our end; safe to retry idempotent reads.

Transport-level errors (auth, routing, rate-limiting) use snake_case codes; resource-level errors use kebab-case (the Firebase callable convention). Always branch on the HTTP status, not the error string.

# Ownership

Every read and write is scoped to the account the API key resolves to. Requesting another account’s link or campaign returns 404 (not 403) — we don’t reveal that it exists.

# Async jobs

Batch and serialized mint jobs accept the request (200, queued), then surface failures in the job status (status: "failed" with a reason), not in the original HTTP response. Always poll the job.

# Verification

The public verification endpoints never error on an unknown serial — they return status: "unknown" (a fake/un-issued code is a meaningful answer, not a 404).

# Next