Skip to Content
APIErrors

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

StatusCodeWhat happened
400BAD_REQUESTA query parameter was malformed — most often an updatedSince that is not an ISO 8601 date.
401UNAUTHENTICATEDNo key, a malformed key, a revoked key or an expired key.
402UPGRADE_REQUIREDThe workspace plan does not include API access. Spectrum and above.
403FORBIDDENThe key is valid but lacks the scope this endpoint requires.
404NOT_FOUNDNo such record in your workspace.
429RATE_LIMITEDToo many requests. Honour Retry-After. Reads and writes are metered separately.
500INTERNAL_ERRORSomething broke on our side. On a read, safe to retry with backoff. On a write, see below.

Endpoint-specific codes

StatusCodeWhat happened
409REGISTRATION_NOT_CHECKABLECheck-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.

StatusCodeWhat happened
400IDEMPOTENCY_KEY_REQUIREDThe write did not carry an Idempotency-Key header. It is required on every write.
400IDEMPOTENCY_KEY_INVALIDThe key was not 1–255 printable ASCII characters with no whitespace.
409IDEMPOTENCY_KEY_REUSEDThis key was already used for a different request. A key identifies one operation.
409IDEMPOTENCY_REQUEST_IN_PROGRESSThe original request under this key is still running, or did not complete. Retry with a new key.
409IDEMPOTENCY_RESPONSE_UNAVAILABLEThe original request succeeded but its response was too large to store. Fetch the record with a GET.
413REQUEST_TOO_LARGEThe 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:

  1. Reconcile — GET the collection to see whether the record exists.
  2. 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.
Last updated on