> ## 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.

# Create a customer

> Creates a customer without any reservation. The restaurant's existing customers are matched first, on `external_id`, then phone number (national and international spellings of a number match), then email (case-insensitive). Replaying a call that succeeded finds the customer it created, as long as it carries an `external_id`, a phone or an email; two identical calls sent at the same time can still both create one. `external_id` is stored on creation and with `on_match: update`, not by a plain `return_existing` match. `on_match` decides what a match does: `return_existing` (default) returns the existing customer untouched, `update` writes the provided fields onto it, `create_new` skips matching and always creates. The response says which happened in `result`: HTTP 201 for `created`, 200 for `matched` and `updated`. A blacklisted customer is returned like any other, with `blacklisted: true`. Requires `CUSTOMERS_WRITE` and `CUSTOMERS_READ` (the response is the full customer).



## OpenAPI

````yaml https://app.eat-now.io/api/partner/v1/openapi.json post /api/partner/v1/customers
openapi: 3.0.3
info:
  title: EatNow Partner API
  version: v1
  description: >-
    Version 1 of the EatNow partner API.


    This API is designed for trusted partner integrations that need
    restaurant-scoped access to availability, reservation management, and
    operational reference data.


    Authentication

    - Use `Authorization: Bearer <token>` with a partner API token created from
    the EatNow dashboard.

    - Access is controlled by credential scopes and each request is resolved to
    a single restaurant.


    Base URL

    - The server URL in this document is the canonical base URL for the current
    environment.


    Errors

    - Endpoint errors use `error.code`, `error.message`, and `error.request_id`.
    Errors raised before the endpoint, including rate limiting, may use another
    format.

    - Validation, authentication, authorization, conflicts, and not-found cases
    all use the same top-level error shape.


    Rate limits

    - The partner route rule allows 600 requests per minute per client IP. Back
    off on 429. When present, x-ratelimit-reset is an epoch timestamp in
    milliseconds.


    Public endpoints

    - `GET /api/partner/v1` and `GET /api/partner/v1/openapi.json` are public.

    - All business endpoints require Bearer authentication.
servers:
  - url: https://app.eat-now.io
security: []
tags:
  - name: Meta
    description: Platform metadata and schema endpoints
  - name: Authentication
    description: Credential validation and auth context endpoints
  - name: Availability
    description: Bookable slot search and exact slot validation
  - name: Catalog
    description: Reference data for restaurant tables, rooms, prescribers, and discounts
  - name: Reservations
    description: Reservation lookup, search, update, and cancellation
  - name: Customers
    description: >-
      Customer (CRM) records, independent of reservations: list, sync, create,
      update
  - name: Webhooks
    description: Webhook endpoints a key subscribes to V1 events (reservations, customers)
  - name: Missed Calls
    description: Report missed calls and trigger WhatsApp follow-up templates
externalDocs:
  description: Integration guides, authentication, errors, and webhook reference
  url: https://docs.eat-now.io/api-reference/introduction
paths:
  /api/partner/v1/customers:
    post:
      tags:
        - Customers
      summary: Create a customer
      description: >-
        Creates a customer without any reservation. The restaurant's existing
        customers are matched first, on `external_id`, then phone number
        (national and international spellings of a number match), then email
        (case-insensitive). Replaying a call that succeeded finds the customer
        it created, as long as it carries an `external_id`, a phone or an email;
        two identical calls sent at the same time can still both create one.
        `external_id` is stored on creation and with `on_match: update`, not by
        a plain `return_existing` match. `on_match` decides what a match does:
        `return_existing` (default) returns the existing customer untouched,
        `update` writes the provided fields onto it, `create_new` skips matching
        and always creates. The response says which happened in `result`: HTTP
        201 for `created`, 200 for `matched` and `updated`. A blacklisted
        customer is returned like any other, with `blacklisted: true`. Requires
        `CUSTOMERS_WRITE` and `CUSTOMERS_READ` (the response is the full
        customer).
      operationId: createPartnerCustomer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerApiCustomerCreateRequest'
            examples:
              upsert:
                summary: Create or update from a CRM
                value:
                  name: Jane Doe
                  email: jane@example.com
                  phone_number: '+33612345678'
                  external_id: crm-4821
                  tags:
                    - VIP
                  newsletter_consent: true
                  on_match: update
      responses:
        '200':
          description: >-
            Existing customer returned (`result: matched`) or updated (`result:
            updated`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerApiCustomerCreateResponse'
              examples:
                matched:
                  summary: Matched on phone number
                  value:
                    data:
                      id: cus_123
                      external_id: crm-4821
                      name: Jane Doe
                      email: jane@example.com
                      phone_number: '+33612345678'
                      country_code: FR
                      lang: FR
                      tags:
                        - VIP
                      note: Prefers the terrace
                      custom_fields:
                        field_birthday:
                          name: Birthday
                          value: '1990-04-12'
                      newsletter_consent: true
                      marketing_opt_outs: []
                      blacklisted: false
                      stats:
                        reservation_count: 7
                        confirmed_count: 6
                        no_show_count: 1
                        feedback_count: 2
                        average_feedback_score: 4.5
                        total_amount_paid: 42000
                        average_basket: 2800
                        last_reservation_date: '2026-09-12'
                      created_at: '2025-11-03T18:22:10.000Z'
                      updated_at: '2026-09-12T21:04:55.000Z'
                    result: matched
        '201':
          description: 'Customer created (`result: created`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerApiCustomerCreateResponse'
        '400':
          description: Request validation failed (see `error.details.issues`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerApiError'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerApiError'
        '403':
          description: >-
            Scope is insufficient. Required: `CUSTOMERS_READ` and
            `CUSTOMERS_WRITE`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerApiError'
        '422':
          description: >-
            A custom field is unknown for this restaurant or its value does not
            fit the field type.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerApiError'
        '429':
          description: >-
            Rate limit exceeded. Configured limit: 600 requests per minute per
            client IP for the partner route rule. This middleware response may
            not use the PartnerApiError envelope or include request_id. Back off
            before retrying.
        '500':
          description: >-
            Unexpected server error. Preserve error.request_id for support.
            Before retrying a write, check whether it already succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerApiError'
      security:
        - PartnerBearerToken: []
components:
  schemas:
    PartnerApiCustomerCreateRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        email:
          type: string
          nullable: true
          format: email
        phone_number:
          type: string
          nullable: true
          minLength: 3
          maxLength: 40
          description: >-
            E.164 (`+33612345678`) recommended. A national number is read with
            the restaurant's country.
        lang:
          type: string
          enum:
            - FR
            - EN
            - IT
            - ES
            - DE
            - PT
          description: Defaults to the restaurant's language.
        external_id:
          type: string
          nullable: true
          minLength: 1
          maxLength: 255
          description: Partner-side identifier of the customer.
        tags:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 100
          maxItems: 100
          description: Replaces all the customer's tags.
        note:
          type: string
          nullable: true
          maxLength: 10000
          description: Internal staff note.
        custom_fields:
          type: object
          additionalProperties:
            anyOf:
              - type: string
              - type: number
              - type: boolean
              - nullable: true
              - type: array
                items:
                  type: string
          description: >-
            Custom field values keyed by field id (see `GET
            /api/partner/v1/customer-fields`). Only the given fields change;
            `null` clears one. The value must fit the field type: TEXT → string,
            NUMBER → number within `min`/`max`, DATE → `YYYY-MM-DD`, BOOLEAN →
            boolean, SELECT → one option value, MULTI_SELECT → an array of
            distinct option values (at most `max_items`).
        newsletter_consent:
          type: boolean
          description: >-
            Newsletter consent. `false` withdraws it; channel opt-outs
            (`marketing_opt_outs`) are managed by EatNow.
        on_match:
          $ref: '#/components/schemas/PartnerApiCustomerOnMatch'
      required:
        - name
      additionalProperties: false
      description: >-
        Customer creation payload. Existing customers are matched first (see
        `on_match`), so a call replayed after it succeeded finds the customer it
        created.
    PartnerApiCustomerCreateResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/PartnerApiCustomerDetail'
        result:
          $ref: '#/components/schemas/PartnerApiCustomerWriteResult'
      required:
        - data
        - result
    PartnerApiError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              $ref: '#/components/schemas/PartnerApiErrorCode'
            message:
              type: string
            request_id:
              type: string
            details:
              type: object
              additionalProperties:
                nullable: true
          required:
            - code
            - message
            - request_id
      required:
        - error
    PartnerApiCustomerOnMatch:
      type: string
      enum:
        - return_existing
        - update
        - create_new
      default: return_existing
      description: >-
        What to do when the provided details match an existing customer of the
        restaurant (same `external_id`, then same phone number, then same
        email): `return_existing` keeps the existing customer untouched and uses
        it; `update` writes the provided fields onto the existing customer;
        `create_new` skips matching and creates another customer anyway.
    PartnerApiCustomerDetail:
      type: object
      properties:
        id:
          type: string
        external_id:
          type: string
          nullable: true
          description: >-
            Partner-side identifier of the customer. Matched first when creating
            a customer.
        name:
          type: string
        email:
          type: string
          nullable: true
          format: email
        phone_number:
          type: string
          nullable: true
          description: >-
            Phone number as stored, spaces removed. E.164 when it was provided
            that way.
        country_code:
          type: string
          nullable: true
        lang:
          type: string
          enum:
            - FR
            - EN
            - IT
            - ES
            - DE
            - PT
        tags:
          type: array
          items:
            type: string
        note:
          type: string
          nullable: true
          description: Internal staff note.
        custom_fields:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/PartnerApiMetadataField'
          description: >-
            Values of the restaurant's customer custom fields, keyed by field id
            (see `GET /api/partner/v1/customer-fields`).
        newsletter_consent:
          type: boolean
        marketing_opt_outs:
          type: array
          items:
            type: string
            enum:
              - EMAIL
              - WHATSAPP
          description: >-
            Channels on which the customer must not receive marketing
            (unsubscribed, bounced, blocked).
        blacklisted:
          type: boolean
          description: >-
            Blacklisted by the restaurant: new reservations for this customer
            are rejected.
        stats:
          $ref: '#/components/schemas/PartnerApiCustomerStats'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - external_id
        - name
        - email
        - phone_number
        - country_code
        - lang
        - tags
        - note
        - custom_fields
        - newsletter_consent
        - marketing_opt_outs
        - blacklisted
        - stats
        - created_at
        - updated_at
    PartnerApiCustomerWriteResult:
      type: string
      enum:
        - created
        - matched
        - updated
      description: >-
        `created`: a new customer (HTTP 201). `matched`: an existing customer,
        left untouched (HTTP 200). `updated`: an existing customer, updated with
        the provided fields (HTTP 200).
    PartnerApiErrorCode:
      type: string
      enum:
        - AUTHENTICATION_REQUIRED
        - INVALID_AUTHENTICATION
        - INSUFFICIENT_SCOPE
        - RESTAURANT_SUSPENDED
        - VALIDATION_ERROR
        - RESOURCE_NOT_FOUND
        - RESOURCE_CONFLICT
        - RATE_LIMITED
        - INTERNAL_ERROR
    PartnerApiMetadataField:
      type: object
      properties:
        name:
          type: string
        value:
          anyOf:
            - type: string
            - type: number
            - type: boolean
            - nullable: true
            - type: array
              items:
                type: string
      required:
        - name
        - value
    PartnerApiCustomerStats:
      type: object
      properties:
        reservation_count:
          type: integer
          description: >-
            Reservations that are neither canceled nor rejected, upcoming ones
            and no-shows included.
        confirmed_count:
          type: integer
          description: '`reservation_count` minus the no-shows.'
        no_show_count:
          type: integer
        feedback_count:
          type: integer
        average_feedback_score:
          type: number
          nullable: true
        total_amount_paid:
          type: integer
          description: >-
            Total paid over the customer's reservations, in the smallest
            currency unit (for example cents).
        average_basket:
          type: number
          description: >-
            `total_amount_paid` divided by the covers of those reservations, in
            the smallest currency unit.
        last_reservation_date:
          type: string
          nullable: true
          description: >-
            Day (`YYYY-MM-DD`, restaurant calendar) of the latest reservation
            that is neither canceled nor rejected. Can be in the future.
      required:
        - reservation_count
        - confirmed_count
        - no_show_count
        - feedback_count
        - average_feedback_score
        - total_amount_paid
        - average_basket
        - last_reservation_date
      description: >-
        Figures computed by EatNow from the customer's reservations. Read-only.
        Their changes do not move `updated_at`.
  securitySchemes:
    PartnerBearerToken:
      type: http
      scheme: bearer
      bearerFormat: API Token
      description: >-
        Restaurant-scoped partner API token passed as `Authorization: Bearer
        <token>`.
      x-default: your_token_here

````