Writes
A write is a request that changes something in your workspace. They behave differently from reads in five ways, and all five are the same for every write we add — so this page is the contract, not a description of one endpoint.
Preview. v1 is Preview. Writes are real and safe to build against; the shape is not yet frozen.
What writes exist today
| Endpoint | Does | Scope |
|---|---|---|
POST /registrations/{id}/check-in | Marks an attendee as arrived | registration.checkin |
One. The read surface is much larger — see the Reference. Check-in went first because it is naturally safe to repeat: the flag only moves one way, so a duplicate scan changes nothing even if the two requests were not linked by a key.
1 · An Idempotency-Key is required
Every write must carry one. A write without it is refused with 400 IDEMPOTENCY_KEY_REQUIRED — the header is not optional and there is no
tolerant mode.
Derive the key from the attempt, not from the record. A fresh UUID per scan is right; the registration id is not. A key derived from the record inherits that record’s history, and a refusal stored against it replays for the rest of the key’s lifetime. See Idempotency.
2 · Write scopes are deliberately narrow
A key’s scope for a write is the specific action, not blanket write access.
registration.checkin lets a badge or kiosk vendor check attendees in without
gaining the ability to create or edit registrations.
Give an integration the narrowest scope that does its job — and expect us to keep splitting scopes this way as more writes arrive, rather than widening one.
3 · Writes have their own, lower rate ceiling
120 requests per minute per key, against 600/minute for reads. See Rate limits for the headers and how to back off.
It is one ceiling on every plan that includes API access. Higher tiers do not buy more write throughput today.
4 · Every write is audited, and the audit cannot go missing
A write records an entry in your workspace’s audit trail, and the change and its audit entry are committed together — if one cannot be written, neither is. So a successful write always has an audit row behind it; there is no window in which something changed and nothing recorded it.
No staff member is named. An API call has no Novex user behind it, so the audit entry attributes the action to the key rather than inventing a person. The registration’s own “checked in by” field is deliberately left empty rather than filled with someone who was not there.
5 · A write does exactly what it says, and nothing else
No email, no notification, no webhook. A check-in through the API does what a check-in at the desk does — writes the record and its audit entry. The desk does not send anything either, and an integration should not quietly do more than the product does.
When two clients race
Two scanner lanes hitting the same attendee at once is a normal event, not an error, and it is handled rather than serialised onto you:
- Both callers get
200with the attendee’s current state. checkedInAtkeeps the first arrival’s instant — the second request does not overwrite it with a later time.- Only one audit entry is written, because only one request actually changed anything.
The same holds for a re-scan seconds later, or a retry over a flaky link: an
attendee who is already checked in returns 200 with the current record, not a
conflict. Treating a successful outcome as an error would turn a working door
into an alerting one.
When something goes wrong
A
500on a write means the outcome is unknown — unlike a read, where it means nothing happened. Do not retry with the same key; reconcile with aGETfirst, then retry with a new key. Errors has the full procedure and every code.
During a problem on our side, write throughput drops sharply rather than
stopping. If our rate-limit store is unreachable we fall back to a much tighter
allowance instead of either refusing everything or letting everything through, so
expect 429s at a volume you would not normally see. Honour Retry-After and
your door keeps moving, slowly.
Next
- Check-in endpoint reference — the request, the response, and every refusal
- Idempotency
- Errors
- Changelog