> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eat-now.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Customers (CRM)

> Create, update and sync the restaurant's customers without going through a reservation.

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:

| Type | Value |
| - | - |
| `TEXT` | string |
| `NUMBER` | number |
| `DATE` | `"YYYY-MM-DD"` |
| `BOOLEAN` | `true` / `false` |
| `SELECT` | one option `value` |
| `MULTI_SELECT` | array of distinct option `value`s |

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:

| `on_match` | Match found | HTTP / `result` |
| - | - | - |
| `return_existing` (default) | returned as is, nothing written | `200` `matched` |
| `update` | the provided fields are written onto it | `200` `updated` |
| `create_new` | ignored: a new customer is always created | `201` `created` |

Without a match, the customer is created: `201` `created`.

```bash theme={null}
curl -X POST 'https://app.eat-now.io/api/partner/v1/customers' \
  -H 'Authorization: Bearer YOUR_API_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Jane Doe",
    "email": "jane@example.com",
    "phone_number": "+33612345678",
    "external_id": "crm-4821",
    "tags": ["VIP"],
    "newsletter_consent": true,
    "on_match": "update"
  }'
```

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](#follow-changes-as-they-happen)
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](/api-reference/webhooks)) 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.
