Errors
Errors use one envelope:
{
"error": {
"code": "FORBIDDEN",
"message": "This key lacks the required scope: events.read"
}
}Match on code, not on message — the wording may be improved at any time,
the codes will not change without a version bump.
Codes
| Status | Code | What happened |
|---|---|---|
| 400 | BAD_REQUEST | A query parameter was malformed — most often an updatedSince that is not an ISO 8601 date. |
| 401 | UNAUTHENTICATED | No key, a malformed key, a revoked key or an expired key. |
| 402 | UPGRADE_REQUIRED | The workspace plan does not include API access. Spectrum and above. |
| 403 | FORBIDDEN | The key is valid but lacks the scope this endpoint requires. |
| 404 | NOT_FOUND | No such record in your workspace. |
| 429 | RATE_LIMITED | Too many requests. Honour Retry-After. Reads and writes are metered separately. |
| 500 | INTERNAL_ERROR | Something broke on our side. On a read, safe to retry with backoff. On a write, see below. |
Endpoint-specific codes
| Status | Code | What happened |
|---|---|---|
| 409 | REGISTRATION_NOT_CHECKABLE | Check-in: the booking is not CONFIRMED — error.details.reason says which (canceled, rejected, waitlisted, no_show). Change its status in Novex, then retry with a new Idempotency-Key. |
⚠️ Unlike the codes above, this one is a property of the record’s state, not
of your request — so the same request becomes valid once the booking is
confirmed. Because refusals are remembered against your Idempotency-Key,
retrying with the same key replays the refusal; use a new one.
Why a cancelled booking is a 409 and not a 404
A 404 would tell you the registration does not exist, which is not true and is
not useful at a door: “we cannot check this person in because the booking was
cancelled” is what the person holding the scanner needs to hear. The trade-off
is that a key holding registration.checkin but not registration.read can tell
“exists but not checkable” from “not here” — within its own workspace only,
and revealing nothing but existence. We think that is the right trade.
Idempotency errors
These apply to write endpoints, which require an
Idempotency-Key on every request — see
Writes for the rest of the write contract.
| Status | Code | What happened |
|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED | The write did not carry an Idempotency-Key header. It is required on every write. |
| 400 | IDEMPOTENCY_KEY_INVALID | The key was not 1–255 printable ASCII characters with no whitespace. |
| 409 | IDEMPOTENCY_KEY_REUSED | This key was already used for a different request. A key identifies one operation. |
| 409 | IDEMPOTENCY_REQUEST_IN_PROGRESS | The original request under this key is still running, or did not complete. Retry with a new key. |
| 409 | IDEMPOTENCY_RESPONSE_UNAVAILABLE | The original request succeeded but its response was too large to store. Fetch the record with a GET. |
| 413 | REQUEST_TOO_LARGE | The request body is larger than this API accepts. |
A 500 on a write is not the same as a 500 on a read
On a read endpoint, a 500 means nothing happened and retrying is safe.
On a write endpoint, a 500 means the outcome is unknown. We cannot tell whether your operation
took effect before the failure, so we keep the reservation for your Idempotency-Key rather than
guess — which means retrying with the same key returns 409 IDEMPOTENCY_REQUEST_IN_PROGRESS, not a
second attempt. That refusal is deliberate: the alternative is a system that occasionally books
twice.
What to do after a 500 on a write:
- Reconcile —
GETthe collection to see whether the record exists. - If it does not, retry with a new
Idempotency-Key.
Never reuse the key of a request whose outcome you do not know.
A note on 404
A 404 means the record does not exist for your workspace. An id belonging
to another workspace is indistinguishable from one that never existed — that is
deliberate, and it is why guessing ids reveals nothing.
Distinguishing 401 from 403
- 401 — we could not accept your key at all.
- 403 — your key is fine, but it does not carry the scope this endpoint needs. Add the scope in Settings → API, or use a key that has it.