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

# Ancien format des webhooks

> Le format des webhooks créés avant la V1 : corps, en-têtes et signature, et comment passer à la V1.

<Warning>
  Ce format est gelé. Il ne concerne que les webhooks créés avant la V1 (leur
  carte affiche **Ancien format** dans Paramètres → Intégrations → Webhooks).
  Tout nouveau webhook, et tout endpoint créé par l’API, utilise la
  [V1](/fr/api-reference/webhooks). Les webhooks à l’ancien format ne reçoivent
  que les événements de réservation : les événements client n’ont pas de corps à
  l’ancien format.
</Warning>

La livraison, les reprises, le journal des livraisons et la liste des parcours
émetteurs sont les mêmes pour les deux formats : voir la
[référence webhooks](/fr/api-reference/webhooks).

## Passer à la V1

1. Adaptez le récepteur pour vérifier `EatNow-Signature` (voir
   [Vérifier la signature](/fr/api-reference/webhooks#vérifier-la-signature))
   et lire le corps V1 : `type` au lieu de `event`, `data.object` (la
   réservation de l’API partenaire : `party_size`, `start_at`…) au lieu de
   `data.reservation`, `data.previous_attributes` (les seuls champs modifiés)
   au lieu de `previous_data`. Le secret ne change pas.
2. Dans **Paramètres → Intégrations → Webhooks**, cliquez sur **Passer au V1**
   sur le webhook. Ce changement est définitif.
3. Les livraisons déjà présentes dans le journal gardent leur corps et leur
   signature à l’ancien format, renvois compris ; tout ce qui est publié
   ensuite est en V1.

## 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. **La clé de déduplication** : identique à chaque reprise et à chaque renvoi manuel. |
| `X-EatNow-Timestamp` | Valeur `timestamp` du corps, instant UTC ISO 8601 (heure de l’événement, pas de la tentative). |
| `X-EatNow-Signature` | `sha256=` suivi du condensat HMAC-SHA256 hexadécimal. |
| `X-EatNow-Delivery-Log-Id` | Entrée de notre journal de livraison pour cette destination, visible dans le tableau de bord. Pour le support ; ne dédupliquez pas dessus. |
| `X-EatNow-Attempt` | Numéro de tentative de cette entrée du journal, à partir de `1`. |
| `X-EatNow-Test` | `true` uniquement pour le bouton « Tester » des réglages. Le payload est une fausse réservation : accusez réception sans rien en faire. |

Les en-têtes personnalisés de la configuration sont appliqués en premier ; ceux
ci-dessus le sont ensuite et l’emportent toujours (noms comparés sans tenir
compte de la casse) : un en-tête personnalisé ne peut pas les remplacer. Tout
en-tête personnalisé dont le nom commence par `X-EatNow-` est ignoré. 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é ».

**Les montants sont en unités mineures** : `total_amount_paid` et
`payments[].amount` sont des entiers en centimes (`5000` = 50,00 dans la devise
du paiement). Divisez par 100 pour les afficher.

`previous_data` accompagne la plupart des modifications. Pour une action d’une
équipe, un appel partenaire ou un automatisme, c’est la réservation complète
avant le changement. Pour un changement venu d’une caisse (NowOS), il porte les
champs propres de la réservation avant le changement (statut, date, heure,
couverts…) avec les relations actuelles (client, tables, salle).

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