Skip to main content
The customers endpoints give a CRM, a marketing tool or a loyalty program direct access to the restaurant’s customer records: no reservation is needed to create or update one. Reading requires 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.
Replaying a call that succeeded finds the customer it created, provided the call carries an 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):
  1. First run: call without updated_since and follow pagination.next_cursor until has_more is false.
  2. Keep the updated_at of the last customer you processed.
  3. Next runs: pass it as updated_since and 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_id there). When omitted, customer.on_match follows the restaurant’s own setting (match unless automatic matching is off). It differs from POST /customers on two points: return_existing may still record a newsletter consent the guest just gave, and update never clears a contact detail (an empty email or phone is ignored).
A blacklisted customer rejects the reservation, whether linked by 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 to customer.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.