Errors
The REST API uses standard HTTP status codes and returns a JSON error body:
{ "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
- Authentication · the full API reference.