CUSTOMERS_READ; creating and updating require
CUSTOMERS_WRITE and CUSTOMERS_READ, because every write returns the full
record. A customer record is personal data from end to end, so there is no
masked view: grant these scopes only to integrations that may hold it.
Customers belong to the key’s restaurant. A group of restaurants has one
customer record per restaurant.
The customer resource
GET /customers/{id} returns the contact details, tags, the internal note,
the custom_fields, the marketing state (newsletter_consent,
marketing_opt_outs), whether the restaurant blacklisted the customer, and
stats computed from their reservations (count, no-shows, amount paid, day of
the latest reservation). stats are read-only.
Custom fields are keyed by field id. GET /customer-fields lists the
restaurant’s fields with their type and, for a list, the allowed values:
A
NUMBER field may be bounded by min / max, a MULTI_SELECT by
max_items. A write with an unknown field or a value that does not fit returns
422, with one entry per faulty field in error.details.issues.
Create a customer
POST /customers looks for an existing customer before creating one, in this
order: same external_id, same phone number (national and international
spellings of a number match), same email (case-insensitive). on_match decides what a match does:
Without a match, the customer is created:
201 created.
external_id, a phone or an email. It is not a lock: two identical
calls sent at the same time can both create a customer, so do not fire retries
in parallel.
external_id is your identifier for the customer: store it, it is matched
first and stays stable when the guest changes phone or email. It is written on
creation and with on_match: "update"; a plain return_existing match leaves
the record, external_id included, untouched. It is separate from the
identifiers other channels keep on the customer (Reserve with Google, for
instance), which the API neither shows nor changes.
A blacklisted customer is returned like any other, with blacklisted: true.
Update a customer
PATCH /customers/{id} changes only the fields it carries; null clears a
field. tags replaces the whole list, custom_fields changes only the fields
it names. newsletter_consent: false withdraws consent. The record is shared by
all the customer’s reservations.
Keep a copy in sync
GET /customers lists customers by updated_at, then id, with cursor
pagination (limit up to 100, default 50):
- First run: call without
updated_sinceand followpagination.next_cursoruntilhas_moreisfalse. - Keep the
updated_atof the last customer you processed. - Next runs: pass it as
updated_sinceand page through again.
updated_since is inclusive: the boundary customer comes back once, so
deduplicate on id. Start each run a few minutes before the last updated_at
you kept: a write that commits late can carry a slightly older timestamp.
Changes to stats do not move updated_at: read a customer again when you need
fresh figures. A new marketing opt-out does move it.
The feed carries customers that exist. A customer deleted in EatNow, or merged
into another one by the staff, disappears from it: the
customer.deleted and customer.merged webhooks
report it. Without webhooks, periodically list all customers (no
updated_since) and drop the ids that no longer come back. GET /customers also filters by
email, phone, external_id and tag.
Customers and reservations
A reservation does not need a prior customer call.POST /reservations takes
either:
customer: { "id": "…" }, to book for a customer you already know;- the customer’s details inline, to find or create the customer in the same
call, matched on phone then email (no
external_idthere). When omitted,customer.on_matchfollows the restaurant’s own setting (match unless automatic matching is off). It differs fromPOST /customerson two points:return_existingmay still record a newsletter consent the guest just gave, andupdatenever clears a contact detail (an empty email or phone is ignored).
id or
matched inline. The customer is resolved only once the reservation passed its
checks, so a rejected reservation does not create or update a customer.
GET /reservations?customer_id=… lists a customer’s reservations.
Follow changes as they happen
Subscribe an endpoint tocustomer.created, customer.updated,
customer.merged and customer.deleted (see the
webhook reference) to learn about changes as they
happen, deletions and merges included. The events carry the same customer
object as GET /customers/{id}. Keep the incremental sync above as the source
of truth: the CSV import publishes no events, and a missed delivery is caught up
by the next sync.