Statuses
The API vocabulary is provider-agnostic:booked → confirmed → arrived → seated → finished, with
no_show and cancelled as exits at any point.
booked= not yet confirmed. Optional to push; never annotates the floor plan on the till.- Cancellation is always
status: "cancelled"viaPUT— there is no DELETE.
PARTIALLY_SEATED sends the actually-occupied tables in tableIds.
Seating is bidirectional
You seat — writestatus: "seated" with tableIds. The API checks
occupancy synchronously (409 table_occupied if the table is taken,
with the occupant in occupiedBy). On success the seat is accepted,
not yet executed: the till opens the order within a couple of seconds
and you receive table.seated carrying your reservationExternalId as
confirmation. If a walk-in grabbed the table in that window, you receive
reservation.seat_rejected instead — re-seat the party; the instruction
will not retry until you change the reservation. A walk-in typed in your
own system is just a reservation that arrives already seated.
Staff seats — the till links the reservation and you learn about it
through the same table.seated. Treat it as you would your own seat
confirmation (e.g. EatNow flips to SEATED).
After the link, the order belongs to the till. Your updates can
still move the party (tableIds → the till executes the move) and
adjust covers, but cancelling the reservation never closes the order —
closing and paying are staff gestures. Staff-side moves come back to you
as table.freed(moved) + table.seated.
Your own webhook reactions echo back as PUTs with a newer updatedAt
and identical content — that is expected and converges: identical
content is a no-op on the till.
Prepayments and imprints
Money you collected upfront travels on the reservation:prepaidMinor(deposit or full prepayment — same mechanics) is spendable on the bill: staff applies it in the payment sheet and it shows up incheck.paidas theprovider_prepaidtender — that is your consumption signal for reconciliation. The till applies at most the amount due.imprintis display-only on the till (a guarantee badge). Releasing or charging it stays entirely on your side — key it oncheck.paid/table.freed(normal payment) andreservation.status_changed(no_show).
Guest profile and CRM
Each reservation carries aguest object and an internalNote. NowOS
never writes the guest record back — you own it. Two notes, two
audiences:
guest.allergies— the guest’s own note (allergies and preferences). Shown to staff on the reservation. Read-only on the till. Send theguestblock whenever you have this note, even for a party you cannot name —guest.nameis nullable precisely so an allergy is never lost for want of a customer record.internalNote(on the reservation, not the guest) — a staff-only note (EatNow’scustom_message). Never shown to the guest. This one is two-way: it lands on the order the booking seats, where it prints on every kitchen ticket, and an edit made at the till comes back to you asreservation.note_changed. Last writer wins onupdatedAt, and you are expected to echo it back like any other change.
The 360° profile (read-back)
NowOS is the transactional system of record: every paid check is pushed to you ascheck.paid, carrying lines[], tenders[], and the
reservationExternalId that ties the spend to the party. You keep the
CRM. Aggregate that history and return it on the guest, so the till can
show a full profile when staff open the reservation.
These guest fields are read-back only — you send them, NowOS displays
them, NowOS never recomputes them. There is no two-way sync of a shared
field: one writer per field.
The loop is:
check.paid → you update the guest’s history on your side →
the next reservation read for that guest carries fresh aggregates → the
till shows them. Leave any aggregate null if you don’t track it; the
till just hides that line.