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

# Référence webhooks

> Événements, payloads, vérification des signatures et limites de livraison.

Les webhooks envoient des requêtes HTTP `POST` à l’URL configurée dans
[Paramètres → Intégrations → Webhooks](/fr/integrations/webhooks). Leur secret
de signature est distinct des clés API. Aucun jeton Bearer ni scope
`WEBHOOKS_WRITE` n’est requis pour les recevoir ; aucune route publique v1 ne
gère leur configuration.

## Événements et couverture

| Événement             | Sens                                                      |
| --------------------- | --------------------------------------------------------- |
| `RESERVATION_CREATED` | Un parcours émetteur a créé une réservation.              |
| `RESERVATION_UPDATED` | Un parcours émetteur a modifié ses données ou son statut. |
| `RESERVATION_DELETED` | Un parcours émetteur a supprimé l’enregistrement.         |

Une configuration active reçoit uniquement les événements sélectionnés. Une
annulation change le statut, sans supprimer la réservation : recherchez
`CANCELED_BY_USER` ou `CANCELED_BY_RESTAURANT` dans une modification si le
parcours concerné en émet une. Il n’existe pas d’événement d’annulation
distinct.

<Warning>
  Ces événements ne couvrent pas toutes les modifications. Certains parcours des
  équipes sur les réservations et les tables, ainsi que des changements
  automatiques de statut ou d’affectation, en émettent. Les écritures par API
  partenaire et les parcours du portail de réservation n’émettent pas
  directement ces webhooks. Une modification automatique ultérieure peut
  produire un événement sans création préalable. N’en faites pas l’unique source
  d’une synchronisation exhaustive.
</Warning>

## En-têtes

| En-tête              | Valeur                                                |
| -------------------- | ----------------------------------------------------- |
| `Content-Type`       | `application/json`                                    |
| `User-Agent`         | `EatNow-Webhooks/1.0`                                 |
| `X-EatNow-Event`     | Valeur `event` du corps.                              |
| `X-EatNow-Delivery`  | Valeur `id` du corps.                                 |
| `X-EatNow-Timestamp` | Valeur `timestamp` du corps, instant UTC ISO 8601.    |
| `X-EatNow-Signature` | `sha256=` suivi du condensat HMAC-SHA256 hexadécimal. |

Ne remplacez pas ces en-têtes dans la configuration personnalisée. La signature
porte sur le **corps brut uniquement**, sans préfixer le timestamp de l’en-tête.

## Payload

Exemple de création avec des identifiants et dates illustratifs :

```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": ""
        }
      }
    }
  }
}
```

| Champ                       | Contrat                                                                                    |
| --------------------------- | ------------------------------------------------------------------------------------------ |
| `id`                        | Identifiant d’envoi, conservé pour les reprises et les destinataires de cette publication. |
| `event`                     | Un des trois noms d’événements ci-dessus.                                                  |
| `timestamp`                 | Instant de construction de l’événement, conservé lors des reprises.                        |
| `restaurant_id`             | Restaurant qui publie l’événement.                                                         |
| `data.reservation`          | État de la réservation pour cet événement.                                                 |
| `previous_data.reservation` | Ancien état facultatif ; absent de certaines modifications.                                |

La réservation contient toujours `id`, `restaurantId`, `status`, `group_size`,
`reservation_date`, `reservation_time`, `created_at`, `updated_at`, `source`,
`tags` et `metadata` (qui peut valoir `null`). Ce format diffère de l’API
partenaire : `group_size` et date/heure séparées remplacent `party_size` et
`start_at`.

`reservation_date` représente le jour de service du restaurant, normalement
sérialisé à minuit ISO. Conservez sa partie `YYYY-MM-DD` ; ne convertissez pas
ce minuit UTC vers un autre fuseau pour en déduire le jour. `reservation_time`
est une heure locale `HH:mm`. Ne traitez pas cette paire comme un instant UTC.
`created_at`, `updated_at` et `timestamp` sont des instants UTC.

Les champs facultatifs dépendent de la réservation :

* `customer` : identifiant, `external_id` nullable, nom et coordonnées/langue
  disponibles.
* `tables` : objets `id` et `name`, sans salle imbriquée.
* `room` : `id` et `name` multilingue, pas une simple chaîne.
* `waiter` : `id` et `name`.
* `payments` : `id`, `amount`, `currency`, `status`, `provider`, `provider_id`
  facultatif et `created_at`.
* `custom_message`, `allergies` et `total_amount_paid` : omis lorsqu’ils sont
  vides ou à zéro par le transformateur actuel. Une absence ne signifie pas «
  inchangé ».
* `shift` : peut apparaître dans le test ; les événements ordinaires l’omettent
  actuellement. N’exigez pas ce champ.

Les messages peuvent contenir des données personnelles et de paiement, quels que
soient les scopes d’une clé API. Limitez l’accès aux corps et aux secrets, et
évitez la journalisation des payloads complets.

## Vérifier la signature

Calculez HMAC-SHA256 sur les **octets exacts reçus**, avec le secret du webhook.
Rejetez une signature absente ou malformée, puis comparez les condensats en
temps constant. Décodez le JSON uniquement après vérification : le reconstruire
peut modifier les espaces ou l’ordre des champs et invalider une signature
légitime.

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

### Exemple de récepteur Express

Enregistrez la fonction Node.js dans `verify-signature.js`. Déclarez la route
avec corps brut **avant** tout middleware `express.json()`. Cet exemple vérifie
et journalise uniquement les identifiants : remplacez ce traitement de
démonstration par votre traitement métier avant d’acquitter un événement en
production. Chargez le secret depuis un stockage serveur sécurisé, sans le
committer.

```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);
```

## Livraison et reprises

* Renvoyez `2xx` dans le délai configuré après traitement réussi ou prise en
  charge durable. Le délai vaut **5 000 ms** par défaut, réglable de **1 000 à
  30 000 ms**.
* Une erreur réseau ou un timeout qui lève une exception déclenche la politique
  de reprise : **5 tentatives au total au maximum**, délai exponentiel de
  facteur 2, borné entre 1 et 10 secondes. Ces réglages ne garantissent pas un
  horaire précis.
* **Les réponses HTTP `4xx` et `5xx` ne déclenchent actuellement pas ces
  reprises.** Elles sont retournées comme résultat en échec sans lever d’erreur
  de tâche.
* L’ordre et l’unicité de livraison ne sont pas garantis. Une reprise conserve
  le corps et l’`id` ; plusieurs destinataires peuvent recevoir ce même
  identifiant. Utilisez `(restaurant_id, id)` pour repérer un événement déjà
  traité, par intégration si plusieurs destinations sont traitées
  indépendamment.
* Les paramètres n’offrent ni journal de livraison ni commande de rejeu. Il
  n’existe pas d’API publique de rejeu.

Une réponse perdue peut provoquer une reprise après votre traitement. Évitez de
répéter une action métier pour un identifiant déjà traité. La signature
authentifie le corps ; elle ne protège pas à elle seule contre le rejeu.

## Tester et diagnostiquer

**Tester** envoie réellement et de façon synchrone un `RESERVATION_CREATED`
fictif, sans créer de réservation. L’exemple utilise une date de service fixe
(`2025-12-25`, sans heure) et inclut un `shift`, contrairement aux événements
ordinaires. Il ne teste ni la tâche d’arrière-plan ni ses reprises.

En cas de signature invalide, contrôlez le secret, les octets bruts, l’ordre des
middlewares et les en-têtes personnalisés. Après régénération du secret, mettez
à jour le récepteur. Des envois déjà programmés peuvent encore utiliser
l’ancien.

Pour un événement absent, contrôlez activation, sélection, couverture des
parcours, URL, statut HTTP et délai d’attente. Transmettez au support le
restaurant, le nom du webhook, l’identifiant d’événement si disponible, l’heure
et le statut HTTP, jamais le secret.
