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

# Webhook reference

> Events, payloads, raw-body signature verification and delivery limits.

Webhooks send HTTP `POST` requests to the URL configured in
[Settings → Integrations → Webhooks](/integrations/webhooks). They use a signing
secret separate from API keys. No Bearer token or `WEBHOOKS_WRITE` scope is
needed to receive them; v1 exposes no public endpoint for managing webhook
configurations.

## Events and coverage

| Event                 | Meaning                                               |
| --------------------- | ----------------------------------------------------- |
| `RESERVATION_CREATED` | A publishing flow created a reservation.              |
| `RESERVATION_UPDATED` | A publishing flow changed reservation data or status. |
| `RESERVATION_DELETED` | A publishing flow deleted the reservation record.     |

An active configuration receives only its selected events. Cancellation is a
status change, not deletion: inspect `CANCELED_BY_USER` or
`CANCELED_BY_RESTAURANT` on an update when that flow publishes one. There is no
separate cancellation event.

<Warning>
  These events are not an exhaustive change feed. Publishing exists in selected
  staff reservation/table operations and automatic status or table assignment
  flows. Partner API writes and booking portal flows do not directly publish
  these generic webhooks. A later automatic change can produce an update without
  a preceding creation event. Do not rely on them as the sole source for
  complete synchronization.
</Warning>

## Headers

| Header               | Value                                                     |
| -------------------- | --------------------------------------------------------- |
| `Content-Type`       | `application/json`                                        |
| `User-Agent`         | `EatNow-Webhooks/1.0`                                     |
| `X-EatNow-Event`     | Same as body `event`.                                     |
| `X-EatNow-Delivery`  | Same as body `id`.                                        |
| `X-EatNow-Timestamp` | Same as body `timestamp`, an ISO 8601 UTC instant.        |
| `X-EatNow-Signature` | `sha256=` followed by the HMAC-SHA256 hexadecimal digest. |

Do not override these headers in custom configuration. The signature covers the
**raw body only**, not a concatenation of the timestamp header and body.

## Payload

Illustrative creation event; IDs and timestamps are examples:

```json theme={null}
{
  "id": "d68fef44-5121-4a97-9d61-a3b4631d465f",
  "event": "RESERVATION_CREATED",
  "timestamp": "2030-05-20T15:00:00.000Z",
  "restaurant_id": "restaurant_example",
  "data": {
    "reservation": {
      "id": "reservation_example",
      "restaurantId": "restaurant_example",
      "status": "CONFIRMED",
      "group_size": 2,
      "reservation_date": "2030-05-20T00:00:00.000Z",
      "reservation_time": "19:30",
      "created_at": "2030-05-20T15:00:00.000Z",
      "updated_at": "2030-05-20T15:00:00.000Z",
      "source": "WEBSITE",
      "tags": [],
      "metadata": null,
      "customer": {
        "id": "customer_example",
        "external_id": null,
        "name": "Example Guest",
        "email": "guest@example.com",
        "lang": "EN"
      },
      "tables": [
        {
          "id": "table_example",
          "name": "T1"
        }
      ],
      "room": {
        "id": "room_example",
        "name": {
          "EN": "Main room",
          "FR": "Salle principale",
          "DE": "",
          "IT": "",
          "ES": "",
          "PT": ""
        }
      }
    }
  }
}
```

| Field                       | Contract                                                                                |
| --------------------------- | --------------------------------------------------------------------------------------- |
| `id`                        | Event delivery identifier; reused across retries and destinations for that publication. |
| `event`                     | One of the three event names above.                                                     |
| `timestamp`                 | Event construction time, not a new timestamp for each attempt.                          |
| `restaurant_id`             | Restaurant publishing the event.                                                        |
| `data.reservation`          | Reservation snapshot for the event.                                                     |
| `previous_data.reservation` | Optional earlier snapshot; not present on every update.                                 |

The reservation always includes `id`, `restaurantId`, `status`, `group_size`,
`reservation_date`, `reservation_time`, `created_at`, `updated_at`, `source`,
`tags` and `metadata` (which can be `null`). This is **not** the Partner API
reservation format: it uses `group_size` and separate date/time fields, not
`party_size` and `start_at`.

`reservation_date` carries the restaurant's service-day label, normally
serialized as an ISO midnight string. Preserve its `YYYY-MM-DD` part; do not
convert that UTC midnight to another timezone to determine the day.
`reservation_time` is a local `HH:mm` value. Do not treat the pair as a UTC
instant. `created_at`, `updated_at` and the event `timestamp` are UTC instants.

Optional content depends on the reservation:

* `customer`: ID, nullable `external_id`, name and available email, phone and
  language.
* `tables`: objects with `id` and `name`; no nested room.
* `room`: `id` and multilingual `name` object, not a string.
* `waiter`: `id` and `name`.
* `payments`: `id`, `amount`, `currency`, `status`, `provider`, optional
  `provider_id` and `created_at`.
* `custom_message`, `allergies` and `total_amount_paid`: omitted when empty or
  zero by the current transformer. Omission does not mean “unchanged”.
* `shift`: may appear in the test sample; ordinary reservation events currently
  omit it. Do not require it.

Webhooks may contain personal and payment-related data regardless of API key
scopes. Restrict access to bodies and secrets; avoid logging complete payloads.

## Verify the signature

Compute HMAC-SHA256 over the **exact bytes received**, using the webhook secret.
Reject missing or malformed signatures, then compare digests in constant time.
Only parse the JSON after verification. Parsing and reserializing JSON changes
whitespace or field order and can invalidate a legitimate signature.

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    import { createHmac, timingSafeEqual } from "node:crypto";

    export function verifySignature(rawBody, signature, secret) {
      if (
        typeof signature !== "string" ||
        signature.length !== 71 ||
        !/^sha256=[a-f0-9]{64}$/.test(signature)
      ) {
        return false;
      }
      const expected = createHmac("sha256", secret).update(rawBody).digest();
      const received = Buffer.from(signature.slice(7), "hex");
      return timingSafeEqual(expected, received);
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hashlib
    import hmac
    import re


    def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
        if not isinstance(signature, str) or not re.fullmatch(r"sha256=[a-f0-9]{64}", signature):
            return False
        expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
        return hmac.compare_digest(expected, signature[7:])
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    <?php
    function verifySignature(string $rawBody, ?string $signature, string $secret): bool {
        if ($signature === null || preg_match('/^sha256=[a-f0-9]{64}$/D', $signature) !== 1) {
            return false;
        }
        return hash_equals(hash_hmac('sha256', $rawBody, $secret), substr($signature, 7));
    }
    ```
  </Tab>
</Tabs>

### Express receiver example

Save the Node.js verifier as `verify-signature.js`. Register the raw-body
webhook route **before** any `express.json()` middleware. This example only
validates and logs event identifiers; replace that demonstration handling with
your processing before acknowledging a production event. Load the real secret
from secure server-side storage rather than committing it.

```javascript theme={null}
import express from "express";
import { verifySignature } from "./verify-signature.js";

const app = express();
const secret = "REPLACE_WITH_YOUR_WEBHOOK_SECRET";

app.post(
  "/eatnow/webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    if (!Buffer.isBuffer(req.body)) return res.sendStatus(400);
    if (!verifySignature(req.body, req.get("X-EatNow-Signature"), secret)) {
      return res.sendStatus(401);
    }
    let payload;
    try {
      payload = JSON.parse(req.body.toString("utf8"));
    } catch (error) {
      console.error("Invalid webhook JSON", error.message);
      return res.sendStatus(400);
    }
    console.info({ event: payload.event, id: payload.id });
    return res.sendStatus(204);
  },
);

app.use(express.json());
app.listen(3000);
```

## Delivery and retries

* The receiver should return `2xx` within the configured timeout, after
  successful processing or durable acceptance. Default timeout is **5,000 ms**,
  configurable from **1,000 to 30,000 ms**.
* Thrown network or timeout errors use the delivery task's retry policy: at most
  **5 attempts total**, exponential backoff with factor 2 and delay bounds of
  1–10 seconds. These are configuration bounds, not a guaranteed schedule.
* **HTTP `4xx` and `5xx` responses do not currently trigger those retries.**
  They are recorded as an unsuccessful result without throwing a task error.
* Delivery order and exactly-once delivery are not guaranteed. A retry keeps the
  same body and `id`; multiple destinations can receive that same ID. Use
  `(restaurant_id, id)` to identify a repeated event in your consumer, scoped to
  the integration when it handles multiple destinations independently.
* The settings interface has no delivery journal or replay control. There is no
  public replay API.

A lost response can cause a retry after your receiver already processed the
event. Avoid repeating a business action for an already handled ID. The
signature establishes body authenticity; it does not itself prevent replay.

## Test and troubleshoot

The **Test** action sends a real, synchronous `RESERVATION_CREATED` request with
fictional data; it does not create a reservation. The sample uses a fixed
service date (`2025-12-25`, date-only) and includes a `shift`, unlike ordinary
events. It does not exercise the background delivery task or its retry behavior.

For a signature failure, check the secret, raw bytes, middleware order and
custom headers. After regenerating a secret, update the receiver. Already queued
attempts may still carry signatures made with the previous secret.

For a missing event, check activation, selected events, publishing coverage,
URL, response status and timeout. Provide support with the restaurant, webhook
name, event ID if available, time and HTTP status; never send the secret.
