ChatbotE-commerce

Créer un assistant WhatsApp de réservation pour restaurant avec n8n, Supabase et IA : le guide complet

Créez un assistant WhatsApp de réservation restaurant avec n8n, Supabase et IA : disponibilité, confirmations, no-shows et reprise humaine.

Un assistant WhatsApp peut aider un restaurant à répondre rapidement aux demandes de réservation, proposer des créneaux, confirmer une table ou rappeler un client avant son arrivée. Mais un système fiable ne consiste pas à laisser une intelligence artificielle répondre seule à chaque message. Il doit associer une compréhension conversationnelle, des règles métier précises, une base de données qui fait autorité et une équipe capable de reprendre la main lorsque la situation l’exige.

Ce guide vous montre comment concevoir une architecture complète avec WhatsApp Business Cloud API, n8n, Supabase et un modèle d’intelligence artificielle. L’objectif n’est pas d’automatiser aveuglément la relation client. Il est de traiter plus vite les demandes simples tout en évitant les doubles réservations, les réponses imprécises sur les allergies, les confirmations impossibles et les erreurs qui dégradent l’expérience en salle.

À retenir : l’intelligence artificielle doit comprendre la demande du client, pas décider seule de la disponibilité. La confirmation d’une réservation doit toujours s’appuyer sur des règles de capacité, des horaires, une base de données fiable et une procédure de reprise humaine pour les exceptions.

Pourquoi un simple chatbot ne suffit pas pour gérer les réservations

Un chatbot classique répond à des questions. Un assistant de réservation doit faire davantage : collecter des informations, vérifier un créneau, appliquer les contraintes de salle, éviter les doublons, enregistrer une réservation, envoyer une confirmation et gérer une modification ou une annulation.

Cette différence est essentielle. Si un client écrit : « Bonsoir, seriez-vous disponibles samedi vers 20 h 30 pour six personnes, avec une personne allergique aux fruits à coque ? », l’assistant doit comprendre la demande. Il ne doit cependant pas promettre une table avant que la disponibilité soit vérifiée. Il ne doit pas non plus répondre que l’allergie peut être prise en charge sans validation par le restaurant.

Une architecture robuste répartit donc les rôles :

  • L’intelligence artificielle interprète le langage naturel et extrait les informations utiles.
  • La base de données conserve les horaires, les capacités, les réservations et les règles d’exploitation.
  • n8n orchestre les messages, les vérifications, les notifications et les relances.
  • L’équipe du restaurant valide les exceptions, les demandes sensibles et les cas qui ne peuvent pas être résolus automatiquement.

Cette séparation évite un problème fréquent : confondre une réponse plausible avec une réponse opérationnellement correcte. Un modèle de langage peut comprendre l’intention d’un client, mais il ne connaît pas spontanément la capacité restante, les fermetures exceptionnelles, la configuration de la salle ou les contraintes de la cuisine.

Définissez une première version volontairement limitée

La tentation est de construire immédiatement un assistant capable de répondre à toutes les questions, d’accepter toutes les réservations, de gérer toutes les langues et de traiter les avis clients. C’est rarement la meilleure approche. Un restaurant obtient généralement un meilleur résultat en commençant par un périmètre clairement délimité.

La première version doit traiter les demandes les plus répétitives et les moins risquées. Elle doit aussi permettre à l’équipe de comprendre le fonctionnement du système, d’identifier les erreurs et d’ajuster les règles sans perturber le service.

  • Répondre aux questions simples sur les horaires, l’adresse et les conditions de réservation.
  • Collecter le nom, le nombre de couverts, la date, l’heure et un moyen de contact.
  • Proposer des créneaux réellement disponibles à partir de la base de données.
  • Créer une demande temporaire avant confirmation définitive.
  • Confirmer une réservation seulement après validation des règles de capacité.
  • Envoyer un rappel avant le service et permettre une annulation simple.
  • Transférer à l’équipe les demandes de groupe, allergies, privatisation ou situations ambiguës.

Dans cette première version, vous pouvez exclure les fonctionnalités qui ajoutent de la complexité sans apporter immédiatement de valeur : paiement d’acompte, plan de table graphique, recommandations personnalisées, connexion à la caisse, gestion automatique des avis ou interactions multilingues avancées. Ces éléments pourront être ajoutés après avoir validé la fiabilité du flux de base.

Choisissez un périmètre par établissement

Un restaurant traditionnel de quarante couverts ne fonctionne pas comme une brasserie de deux cents places, un établissement gastronomique avec menu unique ou un groupe disposant de plusieurs adresses. Avant de créer une base de données ou un workflow n8n, vous devez choisir les règles que votre assistant devra appliquer.

Commencez par répondre à ces questions :

  1. Quels jours et quels services peuvent être réservés via WhatsApp ?
  2. Combien de couverts maximum pouvez-vous accepter par créneau ?
  3. Quelle durée moyenne associez-vous à une table de deux, quatre, six ou huit personnes ?
  4. À partir de combien de personnes faut-il obligatoirement une validation humaine ?
  5. Les demandes de terrasse, de chaise bébé, d’accessibilité ou de gâteau d’anniversaire sont-elles garanties ou simplement notées comme préférences ?
  6. Quelles conditions d’annulation ou de retard doivent être communiquées au client ?
  7. Qui reçoit les demandes que l’assistant ne peut pas traiter seul ?

Transformez l’organisation du restaurant en règles métier

Les règles métier correspondent aux décisions que le restaurant prend habituellement, souvent de manière implicite, avant d’accepter une réservation. Le tutoriel complet les transformera progressivement en données et en conditions contrôlables dans Supabase et n8n.

Cette étape demande un travail de clarification avec la salle, la direction et, lorsque cela est utile, la cuisine. Une automatisation ne peut pas corriger une règle absente ou contradictoire. Elle peut au contraire rendre le problème plus visible et plus rapide.

Règle métier Exemple de décision Automatisation possible Intervention humaine recommandée
Horaires de service Service du soir de 19 h à 22 h 30, avec dernière arrivée à 21 h 45 Proposer uniquement les créneaux autorisés Ouverture exceptionnelle, retard important ou service privatisé
Capacité maximale Quarante couverts pour le service du soir Refuser les créneaux dont la capacité disponible est insuffisante Décision de surcapacité ou adaptation ponctuelle de la salle
Taille de groupe Réservation automatique jusqu’à six personnes Créer une demande standard dans la limite autorisée Groupes, événements, menu imposé ou réservation de plusieurs tables
Durée de table Une table de deux est conservée une heure trente, un groupe de six deux heures Bloquer le créneau suivant selon la durée prévue Service exceptionnellement long, événement ou adaptation de dernière minute
Préférences de placement Terrasse demandée, sans garantie selon la météo et l’occupation Enregistrer la préférence comme note Promesse de placement précis ou arbitrage en cas de demande concurrente
Allergies et restrictions Allergie signalée dans la demande Créer une alerte et enregistrer la note Confirmation de prise en charge, contamination croisée et adaptation de la carte
Annulation Le client annule à plus de vingt-quatre heures du service Libérer le créneau et enregistrer l’événement Litige, acompte, pénalité ou demande hors politique habituelle

Le principe est simple : automatisez ce qui est déterministe et facilement vérifiable. Faites remonter ce qui exige une appréciation humaine, une promesse particulière ou une connaissance contextuelle que le système ne possède pas.

Définissez les situations que l’assistant doit transférer à une personne

Un assistant conversationnel doit savoir reconnaître ses limites. La meilleure réponse n’est pas toujours une réponse automatique : parfois, la bonne réponse consiste à informer le client qu’un membre de l’équipe va vérifier sa demande.

Cette règle protège à la fois le client et le restaurant. Elle réduit le risque de donner une information inexacte, d’accepter une réservation impossible ou de traiter de manière inappropriée une demande sensible.

Point de vigilance : ne laissez jamais l’assistant confirmer automatiquement la prise en charge d’une allergie, d’un régime médical, d’une demande de privatisation ou d’un groupe important. Il peut enregistrer la demande et prévenir l’équipe, mais la réponse définitive doit venir d’une personne habilitée à vérifier les conditions réelles.

Dans la version initiale, configurez un transfert humain obligatoire pour les demandes suivantes :

  • Allergie sévère, mention d’anaphylaxie, contamination croisée ou demande liée à un régime médical.
  • Groupe dépassant la taille maximale autorisée pour la réservation automatique.
  • Privatisation, événement d’entreprise, anniversaire avec prestation spécifique ou demande de devis.
  • Demande de modification lorsque plusieurs réservations similaires existent pour le même client.
  • Plainte, litige, insatisfaction, erreur de facturation ou contestation d’une annulation.
  • Demande ambiguë, informations contradictoires ou absence d’éléments essentiels.
  • Message manifestement frauduleux, agressif, inapproprié ou susceptible d’exiger une intervention urgente.

Le transfert doit être visible pour l’équipe. Dans les prochaines parties, vous créerez une file de traitement humain et des notifications. En attendant, définissez déjà le délai de réponse attendu et la personne responsable. Une escalade sans propriétaire clair ne résout pas le problème : elle déplace simplement l’attente du client.

Choisissez une architecture où chaque outil a un rôle précis

L’architecture proposée repose sur quatre composants complémentaires. WhatsApp sert de canal de conversation. n8n orchestre les workflows. Supabase conserve les données et applique les règles de disponibilité. Un modèle d’intelligence artificielle interprète les messages en langage naturel et aide à produire une sortie structurée.

Client
  ↓
WhatsApp Business Cloud API
  ↓
Webhook n8n
  ↓
Normalisation du message et identification du contact
  ↓
IA : intention et informations structurées
  ↓
Règles métier et disponibilité dans Supabase
  ↓
Réponse WhatsApp, création de réservation ou transfert humain
  ↓
Journalisation, rappels et suivi opérationnel

WhatsApp Business Cloud API : le canal de conversation

WhatsApp Business Cloud API permet de recevoir des messages et des événements via webhook, puis d’envoyer des réponses à partir de l’infrastructure Meta. Le webhook correspond à l’URL publique que Meta appelle lorsqu’un message est reçu ou lorsqu’un événement lié à un message doit être signalé.

Le restaurant ne doit pas considérer WhatsApp comme une simple boîte de réception. Dans l’architecture cible, le canal devient le point d’entrée d’un processus. Chaque message reçu doit être associé à un contact, à une conversation, à une intention probable et, si nécessaire, à une réservation existante.

n8n : l’orchestrateur du parcours

n8n reçoit les messages, déclenche les branches du workflow, appelle les services externes, applique des conditions et envoie les réponses. Il jouera le rôle de chef d’orchestre entre WhatsApp, Supabase, le modèle d’IA et les outils utilisés par l’équipe.

Lors du développement, n8n fournit une URL de test pour visualiser les données reçues et déboguer le workflow. Cette URL de test n’est pas adaptée à une intégration durable. Une fois le workflow validé, vous devrez activer le workflow et utiliser son URL de production dans votre configuration WhatsApp.

Supabase : la source de vérité opérationnelle

Supabase fournit une base PostgreSQL qui peut stocker les établissements, les horaires, les services, les capacités, les tables, les réservations, les événements de réservation et les états de conversation. Dans ce guide, Supabase ne servira pas seulement à enregistrer des informations : il constituera la source de vérité pour savoir si un créneau peut réellement être proposé ou confirmé.

Cette distinction protège le restaurant contre les incohérences. L’IA peut extraire « deux personnes vendredi à 20 h », mais elle ne doit pas répondre « votre table est confirmée » avant qu’une règle de disponibilité ait vérifié la capacité, l’horaire, les fermetures exceptionnelles et les réservations déjà enregistrées.

Le modèle d’intelligence artificielle : comprendre, jamais inventer

Le modèle d’IA sera utilisé dans un périmètre volontairement limité. Il devra reconnaître l’intention du client, extraire des données comme la date, l’heure, le nombre de couverts ou une préférence de placement, puis produire un format structuré exploitable par le workflow.

Il ne devra pas inventer de disponibilité, de politique d’annulation, de prix, de menu, de délai de réponse ou de réponse médicale. Lorsque les données sont manquantes ou ambiguës, il devra demander une précision ou déclencher une reprise humaine.

Cartographiez le parcours du client avant de créer le premier workflow

Le parcours de réservation doit être décrit comme une succession d’états. Cette méthode évite de créer un assistant qui pose plusieurs fois la même question ou oublie une information déjà fournie.

Voici un parcours de référence pour la première version :

  1. Le client envoie un premier message sur WhatsApp.
  2. Le workflow identifie s’il s’agit d’une réservation, d’une modification, d’une annulation, d’une question fréquente ou d’un cas nécessitant une personne.
  3. Pour une demande de réservation, le système extrait ou demande le nombre de couverts, la date, l’heure, le nom et un moyen de contact.
  4. Le workflow vérifie que les données reçues sont complètes et dans un format utilisable.
  5. Supabase calcule la disponibilité réelle selon les règles du restaurant.
  6. Le système propose un ou plusieurs créneaux, ou indique qu’une vérification humaine est nécessaire.
  7. Le client confirme explicitement le créneau proposé.
  8. Le workflow crée une réservation confirmée, enregistre l’événement et envoie un message récapitulatif.
  9. Avant le service, le système envoie un rappel selon la politique définie.
  10. Le client peut confirmer, modifier ou annuler, avec une reprise humaine lorsqu’une décision ne peut pas être automatisée.

Chaque étape devra laisser une trace dans la base : date de création, message reçu, proposition effectuée, confirmation, modification, annulation ou transfert humain. Cette journalisation sera utile pour comprendre un incident, améliorer les réponses et mesurer la charge réellement traitée par l’assistant.

Protégez les données avant de chercher à enrichir la conversation

Une réservation comporte généralement des données personnelles : nom, numéro de téléphone, date de venue, nombre de couverts et demandes particulières. Vous devez donc collecter uniquement ce qui est nécessaire au service, limiter les accès et définir une durée de conservation cohérente avec vos obligations et vos besoins opérationnels.

Supabase s’appuie sur PostgreSQL et propose notamment le Row Level Security, ou RLS. Ce mécanisme permet de définir des règles d’accès au niveau des lignes de base de données. Dans ce projet, il pourra aider à différencier les droits d’un administrateur, d’un membre de salle, d’un restaurant précis dans un environnement multi-sites et d’un processus serveur contrôlé par n8n.

Bon réflexe : conservez les secrets, les clés d’API et les accès privilégiés dans les identifiants sécurisés de n8n ou dans des variables d’environnement protégées. Ne placez jamais une clé d’administration Supabase, un jeton WhatsApp ou une clé de fournisseur IA dans un front-end public, un message client, une documentation partagée sans contrôle ou une capture d’écran.

La minimisation des données améliore aussi l’exploitation. Demander trop d’informations dès le premier message augmente les abandons et complique le traitement. Pour une réservation standard, le restaurant a généralement besoin d’éléments simples : date, heure, nombre de personnes, nom, moyen de contact et éventuelle demande particulière. Les détails supplémentaires doivent être collectés uniquement lorsqu’ils sont réellement nécessaires.

Votre checklist avant de passer à Supabase et n8n

Avant de créer une table, un webhook ou un prompt IA, validez les décisions suivantes avec les personnes qui exploitent réellement le restaurant.

  • Définir les établissements et services inclus dans la première version.
  • Documenter les horaires, fermetures exceptionnelles et derniers créneaux réservable.
  • Fixer la capacité maximale et les règles de durée par taille de groupe.
  • Choisir la taille maximale de groupe autorisée sans validation humaine.
  • Déterminer les préférences qui peuvent être enregistrées sans être garanties.
  • Établir les cas qui déclenchent obligatoirement un transfert à une personne.
  • Définir les règles de confirmation, d’annulation, de retard et de rappel.
  • Choisir une source de vérité unique pour la disponibilité pendant le projet pilote.
  • Identifier le responsable opérationnel des réservations transférées.
  • Prévoir un environnement de test avec des réservations fictives avant toute utilisation réelle.
  • Répertorier les accès, clés, secrets et données personnelles nécessaires au fonctionnement.
  • Définir les indicateurs qui permettront d’évaluer le pilote sans promettre de résultat automatique.

Vous disposez maintenant du cadre nécessaire pour construire une solution cohérente. La prochaine partie portera sur Supabase : création du projet, schéma de données, tables de réservation, contraintes de capacité, politiques d’accès et premières fonctions PostgreSQL pour calculer la disponibilité sans créer de doubles réservations.

Questions fréquentes

Peut-on utiliser ce système si le restaurant possède déjà un logiciel de réservation ?

Oui, à condition de définir une source de vérité unique pour les disponibilités. Si un logiciel de réservation existant gère déjà les tables et les créneaux, il est généralement préférable de l’intégrer via une API ou un export fiable plutôt que de maintenir deux calendriers indépendants. Si aucune intégration n’est possible, commencez par une assistance qui collecte les demandes et transfère les confirmations à l’équipe.

L’assistant peut-il confirmer une réservation automatiquement ?

Oui, mais uniquement pour les scénarios où les règles sont entièrement vérifiables : créneau ouvert, capacité disponible, taille de groupe dans la limite autorisée, informations complètes et absence de signal nécessitant une validation humaine. Les allergies, groupes importants, événements et demandes ambiguës doivent être transférés à une personne.

Faut-il obligatoirement utiliser Supabase ?

Non. Supabase est retenu dans ce guide parce qu’il fournit une base PostgreSQL, des mécanismes de sécurité et une interface pratique pour structurer les données. Une autre base PostgreSQL ou un logiciel de réservation déjà connecté peut convenir, à condition d’offrir une source de vérité fiable et des accès contrôlés.

Peut-on commencer avec Google Sheets ?

Un tableur peut convenir à une démonstration limitée. Il devient toutefois fragile lorsque plusieurs demandes arrivent simultanément, lorsque les règles de capacité se complexifient ou lorsque vous devez éviter les doubles réservations. Pour un système de réservation exploité au quotidien, une base relationnelle constitue généralement une approche plus robuste.

Conclusion de la partie 1

Un assistant WhatsApp de réservation utile ne commence pas par un prompt, mais par des règles de salle compréhensibles. Vous devez savoir quels créneaux peuvent être proposés, quelles situations restent humaines, quelles données sont nécessaires et quelle source de vérité décide de la disponibilité.

Dans la prochaine partie, vous allez traduire ces décisions dans Supabase. Vous créerez le modèle de données, les tables essentielles, les statuts de réservation et les premières règles de sécurité. Cette étape déterminera si votre futur assistant peut gérer les demandes de manière cohérente ou s’il risque de créer des réservations contradictoires.

Explorer les analyses sur l’IA et l’e-commerce

Sources et pour aller plus loin

Partie 2 : créer la base Supabase pour gérer les réservations sans double créneau

Schéma de données reliant les services, les tables, les clients et les réservations d’un restaurant
Schéma de données reliant les services, les tables, les clients et les réservations d’un restaurant

Dans la première partie de ce guide, vous avez défini les règles métier de votre restaurant : horaires, capacité, durée de table, gestion des groupes, demandes sensibles et conditions de confirmation. Vous allez maintenant traduire ces règles dans une base Supabase.

Cette étape est déterminante. Un assistant WhatsApp peut comprendre qu’un client souhaite une table pour quatre personnes vendredi à 20 h. n8n peut transmettre la demande. Mais aucune de ces briques ne doit décider seule qu’une table est disponible. Cette décision doit venir d’une source de vérité capable de vérifier les réservations existantes, les fermetures exceptionnelles, les capacités et les règles que vous avez choisies.

À retenir : Supabase ne sert pas uniquement à enregistrer les réservations après coup. Dans cette architecture, la base PostgreSQL doit empêcher les incohérences, calculer la disponibilité et conserver l’historique des décisions prises par le workflow.

Pourquoi la base de données doit décider de la disponibilité

Une réservation ne correspond pas seulement à une date et une heure. Elle représente une période pendant laquelle une table, une zone de salle ou une part de la capacité du restaurant est engagée. Si deux demandes arrivent simultanément, un workflow mal conçu peut voir les deux créneaux comme disponibles, puis créer deux réservations incompatibles.

Ce type d’erreur arrive lorsque la disponibilité est calculée uniquement dans l’automatisation, sans contrainte en base. Par exemple, n8n vérifie qu’une table est libre, puis attend quelques secondes avant d’écrire la réservation. Entre ces deux actions, une autre demande peut arriver et obtenir le même résultat.

PostgreSQL propose des types de plages temporelles et des contraintes d’exclusion. Dans un système de réservation, cette combinaison permet d’exprimer une règle concrète : une même table ne peut pas être associée à deux réservations actives qui se chevauchent. Si une tentative de doublon se produit, la base refuse l’écriture au lieu de laisser le problème se propager.

Ce mécanisme ne remplace pas toutes les règles métier. Vous devez toujours définir les horaires de service, les tailles de groupe, les tables combinables et les exceptions. Il crée toutefois un filet de sécurité essentiel contre les conflits de réservation.

Le modèle de données retenu pour ce tutoriel

Le modèle proposé fonctionne pour un restaurant indépendant, mais prévoit déjà une colonne restaurant_id afin de faciliter une évolution vers plusieurs établissements. Vous pouvez l’utiliser sans activer immédiatement la dimension multi-sites.

La structure repose sur huit ensembles de données :

  • restaurants : les informations de chaque établissement, son fuseau horaire et ses paramètres généraux.
  • service_periods : les services récurrents, par exemple déjeuner, dîner, brunch ou service continu.
  • restaurant_tables : les tables physiques, leurs capacités et leurs caractéristiques.
  • blocked_slots : les fermetures exceptionnelles, privatisations et réductions de capacité.
  • customers : les informations minimales nécessaires pour reconnaître un client et suivre sa réservation.
  • reservations : les demandes, réservations temporaires, confirmations, annulations et no-shows.
  • reservation_table_assignments : l’association entre une réservation et une ou plusieurs tables.
  • reservation_events : l’historique des actions importantes, utile pour comprendre ce qui s’est produit.

Dans cette première architecture, les données conversationnelles complètes ne sont pas stockées dans la table des réservations. Vous créerez plus tard une table d’état de conversation, limitée aux informations nécessaires à la reprise d’un échange WhatsApp. Cette séparation évite de transformer le CRM de réservation en archive non maîtrisée de messages.

Préparez un projet Supabase de développement

Commencez dans un projet Supabase distinct de votre futur environnement de production. Vous devez pouvoir créer, modifier et supprimer des données de test sans risquer d’altérer des réservations réelles.

  1. Créez un nouveau projet Supabase dédié au développement.
  2. Choisissez une région cohérente avec vos contraintes opérationnelles et de données.
  3. Conservez les identifiants du projet dans un gestionnaire de secrets ou dans les identifiants sécurisés de n8n.
  4. Ouvrez l’éditeur SQL de Supabase.
  5. Créez un nouveau script nommé, par exemple, 01_schema_reservations.sql.
  6. Exécutez les scripts de cette partie par étapes, et vérifiez chaque résultat avant de continuer.

Ne placez jamais une clé de service Supabase dans une application front-end, dans un message WhatsApp, dans un bloc d’article public ou dans un export de workflow partagé. Les accès privilégiés doivent rester côté serveur, par exemple dans n8n ou dans une fonction sécurisée.

Point de vigilance : les commandes SQL suivantes créent des tables, des types et des fonctions. Exécutez-les dans un projet de test. Avant de les utiliser en production, adaptez-les à vos propres règles de capacité, de conservation des données et de contrôle des accès.

Créez les extensions et les types nécessaires

PostgreSQL utilise des extensions pour apporter certaines fonctionnalités. Pour empêcher les chevauchements de réservation sur une même table, vous allez utiliser une contrainte d’exclusion avec un index GiST. L’extension btree_gist permet notamment de combiner l’égalité sur un identifiant de table et le chevauchement sur une plage horaire.

create extension if not exists btree_gist;

Vous allez ensuite créer des types énumérés. Ils limitent les valeurs acceptées dans certains champs importants, par exemple le statut d’une réservation. Cette approche évite que plusieurs workflows écrivent des variations incohérentes telles que « confirmée », « confirme », « validée » ou « ok ».

create type public.reservation_status as enum (
  'pending',
  'hold',
  'confirmed',
  'cancelled',
  'completed',
  'no_show',
  'needs_human_review',
  'expired'
);

create type public.reservation_source as enum (
  'whatsapp',
  'phone',
  'walk_in',
  'website',
  'manual',
  'other'
);

create type public.reservation_event_type as enum (
  'created',
  'updated',
  'hold_created',
  'confirmed',
  'cancelled',
  'expired',
  'no_show_marked',
  'human_review_requested',
  'human_review_completed',
  'message_sent',
  'message_received'
);

Les statuts proposés servent de référence pour ce tutoriel. Vous pouvez les adapter, mais évitez de modifier leur signification en cours de projet. Un workflow n8n, un tableau de bord et une équipe de salle doivent tous interpréter un statut de la même manière.

Créez les tables fondamentales

Copiez le script suivant dans l’éditeur SQL Supabase. Il crée les tables principales du système. Les commentaires inclus dans le script décrivent le rôle de chaque bloc ; ils ne s’afficheront pas dans votre interface de réservation.

create table public.restaurants (
  id uuid primary key default gen_random_uuid(),
  name text not null,
  timezone text not null default 'Europe/Paris',
  phone text,
  email text,
  address text,
  default_reservation_duration_minutes integer not null default 90
    check (default_reservation_duration_minutes between 30 and 360),
  max_party_size_for_auto_confirmation integer not null default 6
    check (max_party_size_for_auto_confirmation between 1 and 30),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table public.service_periods (
  id uuid primary key default gen_random_uuid(),
  restaurant_id uuid not null references public.restaurants(id) on delete cascade,
  name text not null,
  weekday smallint not null check (weekday between 0 and 6),
  start_time time not null,
  end_time time not null,
  last_booking_time time not null,
  is_active boolean not null default true,
  default_duration_minutes integer not null default 90
    check (default_duration_minutes between 30 and 360),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  check (end_time > start_time),
  check (last_booking_time >= start_time),
  check (last_booking_time <= end_time)
);

create table public.restaurant_tables (
  id uuid primary key default gen_random_uuid(),
  restaurant_id uuid not null references public.restaurants(id) on delete cascade,
  label text not null,
  min_capacity integer not null default 1 check (min_capacity >= 1),
  max_capacity integer not null check (max_capacity >= min_capacity),
  zone text,
  is_accessible boolean not null default false,
  is_outdoor boolean not null default false,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (restaurant_id, label)
);

create table public.blocked_slots (
  id uuid primary key default gen_random_uuid(),
  restaurant_id uuid not null references public.restaurants(id) on delete cascade,
  starts_at timestamptz not null,
  ends_at timestamptz not null,
  reason text not null,
  affects_all_tables boolean not null default true,
  created_at timestamptz not null default now(),
  check (ends_at > starts_at)
);

create table public.customers (
  id uuid primary key default gen_random_uuid(),
  restaurant_id uuid not null references public.restaurants(id) on delete cascade,
  whatsapp_phone text not null,
  full_name text,
  email text,
  language_code text,
  marketing_consent boolean not null default false,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (restaurant_id, whatsapp_phone)
);

create table public.reservations (
  id uuid primary key default gen_random_uuid(),
  restaurant_id uuid not null references public.restaurants(id) on delete cascade,
  customer_id uuid references public.customers(id) on delete set null,
  status public.reservation_status not null default 'pending',
  source public.reservation_source not null default 'whatsapp',
  party_size integer not null check (party_size between 1 and 50),
  starts_at timestamptz not null,
  ends_at timestamptz not null,
  customer_name text,
  customer_phone text,
  customer_email text,
  language_code text,
  seating_preference text,
  special_request text,
  allergy_notice text,
  needs_human_review boolean not null default false,
  hold_expires_at timestamptz,
  confirmation_code text unique,
  created_by text not null default 'system',
  updated_by text not null default 'system',
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  check (ends_at > starts_at),
  check (
    (status = 'hold' and hold_expires_at is not null)
    or status <> 'hold'
  )
);

create table public.reservation_table_assignments (
  id uuid primary key default gen_random_uuid(),
  reservation_id uuid not null references public.reservations(id) on delete cascade,
  restaurant_table_id uuid not null references public.restaurant_tables(id) on delete restrict,
  starts_at timestamptz not null,
  ends_at timestamptz not null,
  created_at timestamptz not null default now(),
  check (ends_at > starts_at),
  unique (reservation_id, restaurant_table_id)
);

create table public.reservation_events (
  id uuid primary key default gen_random_uuid(),
  reservation_id uuid not null references public.reservations(id) on delete cascade,
  event_type public.reservation_event_type not null,
  actor_type text not null,
  actor_reference text,
  payload jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now()
);

create index service_periods_restaurant_weekday_idx
  on public.service_periods (restaurant_id, weekday)
  where is_active = true;

create index restaurant_tables_restaurant_active_idx
  on public.restaurant_tables (restaurant_id)
  where is_active = true;

create index reservations_restaurant_starts_at_idx
  on public.reservations (restaurant_id, starts_at);

create index reservations_customer_id_idx
  on public.reservations (customer_id);

create index reservation_events_reservation_id_created_at_idx
  on public.reservation_events (reservation_id, created_at desc);

La table reservations enregistre la demande globale. La table reservation_table_assignments représente ensuite l’occupation réelle de chaque table physique. Cette séparation est volontaire : une réservation de huit personnes peut, plus tard, être affectée à deux tables si l’équipe autorise cette combinaison.

Dans la première version, n’autorisez pas automatiquement les combinaisons complexes. Vous pouvez affecter une réservation standard à une table unique et transférer les groupes à l’équipe. Cette règle réduit fortement les risques au démarrage.

Empêchez les chevauchements sur une même table

Vous allez maintenant ajouter la contrainte qui empêche deux réservations actives d’occuper la même table au même moment. Cette protection est appliquée au niveau de PostgreSQL : elle reste active même si deux workflows n8n tentent de réserver simultanément la même table.

Exécutez ce script après la création des tables :

alter table public.reservation_table_assignments
add constraint reservation_table_assignments_no_overlap
exclude using gist (
  restaurant_table_id with =,
  tstzrange(starts_at, ends_at, '[)') with &&
);

La syntaxe [) signifie que l’heure de début est incluse, tandis que l’heure de fin est exclue. Une réservation de 19 h à 20 h 30 occupe donc la table jusqu’à 20 h 30 sans bloquer une nouvelle réservation qui commencerait exactement à 20 h 30, si votre règle métier autorise cette rotation.

La contrainte utilise l’opérateur &&, qui signifie « chevauchement ». PostgreSQL refusera l’insertion si une table identique possède déjà une plage horaire qui recouvre, même partiellement, la nouvelle plage demandée.

Cette contrainte protège l’affectation de table. Elle ne vérifie pas encore si le restaurant est ouvert, si le service est autorisé ou si la table possède une capacité suffisante. Vous allez traiter ces contrôles avec une fonction de disponibilité.

Pourquoi cette protection est plus fiable qu’un simple contrôle n8n

n8n peut rechercher les réservations existantes avant de créer une affectation. Ce contrôle reste nécessaire pour proposer une réponse agréable au client. Mais il ne suffit pas à prévenir une collision : deux workflows peuvent effectuer la même recherche à quelques millisecondes d’intervalle, puis tenter chacun de créer une réservation.

La contrainte PostgreSQL agit comme une dernière barrière. Le premier workflow qui écrit une réservation valide réussit. Le second reçoit une erreur de conflit. n8n devra alors proposer un autre créneau ou transmettre la demande à l’équipe, au lieu de confirmer deux clients pour la même table.

Ajoutez des données de démonstration

Pour tester l’architecture, commencez avec un restaurant fictif. Les données suivantes ne doivent pas être utilisées telles quelles en production. Elles servent uniquement à vérifier le schéma et les contraintes avant de saisir vos propres paramètres.

insert into public.restaurants (
  name,
  timezone,
  phone,
  email,
  address,
  default_reservation_duration_minutes,
  max_party_size_for_auto_confirmation
)
values (
  'Restaurant Démonstration',
  'Europe/Paris',
  '+33100000000',
  'contact@restaurant-demonstration.test',
  '1 rue de la Démonstration, 75000 Paris',
  90,
  6
)
returning id;

Copiez l’identifiant retourné par Supabase, puis utilisez-le à la place de RESTAURANT_ID_A_REMPLACER dans votre environnement de test. Ce placeholder ne doit pas apparaître dans une intégration n8n de production : vous utiliserez alors une variable ou une valeur réellement configurée.

insert into public.service_periods (
  restaurant_id,
  name,
  weekday,
  start_time,
  end_time,
  last_booking_time,
  default_duration_minutes
)
values
  ('RESTAURANT_ID_A_REMPLACER', 'Déjeuner', 1, '12:00', '14:30', '13:30', 90),
  ('RESTAURANT_ID_A_REMPLACER', 'Dîner', 1, '19:00', '23:00', '21:45', 105),
  ('RESTAURANT_ID_A_REMPLACER', 'Déjeuner', 2, '12:00', '14:30', '13:30', 90),
  ('RESTAURANT_ID_A_REMPLACER', 'Dîner', 2, '19:00', '23:00', '21:45', 105),
  ('RESTAURANT_ID_A_REMPLACER', 'Déjeuner', 3, '12:00', '14:30', '13:30', 90),
  ('RESTAURANT_ID_A_REMPLACER', 'Dîner', 3, '19:00', '23:00', '21:45', 105),
  ('RESTAURANT_ID_A_REMPLACER', 'Déjeuner', 4, '12:00', '14:30', '13:30', 90),
  ('RESTAURANT_ID_A_REMPLACER', 'Dîner', 4, '19:00', '23:00', '21:45', 105),
  ('RESTAURANT_ID_A_REMPLACER', 'Déjeuner', 5, '12:00', '14:30', '13:30', 90),
  ('RESTAURANT_ID_A_REMPLACER', 'Dîner', 5, '19:00', '23:00', '22:00', 105),
  ('RESTAURANT_ID_A_REMPLACER', 'Dîner', 6, '19:00', '23:00', '22:00', 105);

Dans PostgreSQL, les jours de la semaine sont représentés ici par 0 pour dimanche, 1 pour lundi et 6 pour samedi. Vérifiez que cette convention est bien celle que vous utilisez dans la suite de vos workflows.

Ajoutez ensuite quelques tables de démonstration :

insert into public.restaurant_tables (
  restaurant_id,
  label,
  min_capacity,
  max_capacity,
  zone,
  is_accessible,
  is_outdoor
)
values
  ('RESTAURANT_ID_A_REMPLACER', 'T01', 1, 2, 'Salle principale', true, false),
  ('RESTAURANT_ID_A_REMPLACER', 'T02', 1, 2, 'Salle principale', false, false),
  ('RESTAURANT_ID_A_REMPLACER', 'T03', 2, 4, 'Fenêtre', false, false),
  ('RESTAURANT_ID_A_REMPLACER', 'T04', 2, 4, 'Terrasse', false, true),
  ('RESTAURANT_ID_A_REMPLACER', 'T05', 4, 6, 'Salle principale', true, false);

Ces tables sont volontairement simples. La partie suivante du guide utilisera leurs capacités pour proposer un créneau. Si votre restaurant ne gère pas les tables individuellement, vous pourrez simplifier le modèle en travaillant avec une capacité globale par service. Cette approche est moins précise, mais peut convenir à un premier pilote dans un établissement où la salle est gérée de manière flexible.

Créez une fonction de disponibilité utilisable par n8n

Vous allez maintenant créer une fonction PostgreSQL qui recherche les tables disponibles pour une demande donnée. Cette fonction ne confirme aucune réservation. Elle répond uniquement à une question : quelles tables actives peuvent accueillir ce groupe pendant cette période, sans chevauchement avec une réservation déjà affectée et sans blocage global du restaurant ?

Le choix de centraliser cette logique dans Supabase présente deux avantages. Premièrement, n8n appelle une fonction unique au lieu de reconstruire plusieurs filtres complexes. Deuxièmement, vous pouvez faire évoluer les règles sans modifier tous les workflows qui utilisent la disponibilité.

create or replace function public.find_available_tables(
  p_restaurant_id uuid,
  p_starts_at timestamptz,
  p_ends_at timestamptz,
  p_party_size integer,
  p_prefer_outdoor boolean default false,
  p_require_accessible boolean default false
)
returns table (
  table_id uuid,
  table_label text,
  max_capacity integer,
  zone text,
  is_outdoor boolean,
  is_accessible boolean
)
language sql
stable
as $$
  select
    rt.id as table_id,
    rt.label as table_label,
    rt.max_capacity,
    rt.zone,
    rt.is_outdoor,
    rt.is_accessible
  from public.restaurant_tables rt
  where rt.restaurant_id = p_restaurant_id
    and rt.is_active = true
    and rt.max_capacity >= p_party_size
    and (
      p_prefer_outdoor = false
      or rt.is_outdoor = true
    )
    and (
      p_require_accessible = false
      or rt.is_accessible = true
    )
    and not exists (
      select 1
      from public.blocked_slots bs
      where bs.restaurant_id = p_restaurant_id
        and bs.affects_all_tables = true
        and tstzrange(bs.starts_at, bs.ends_at, '[)')
            && tstzrange(p_starts_at, p_ends_at, '[)')
    )
    and not exists (
      select 1
      from public.reservation_table_assignments rta
      join public.reservations r
        on r.id = rta.reservation_id
      where rta.restaurant_table_id = rt.id
        and r.status in ('hold', 'confirmed')
        and (
          r.status <> 'hold'
          or r.hold_expires_at is null
          or r.hold_expires_at > now()
        )
        and tstzrange(rta.starts_at, rta.ends_at, '[)')
            && tstzrange(p_starts_at, p_ends_at, '[)')
    )
  order by
    case when rt.max_capacity = p_party_size then 0 else 1 end,
    rt.max_capacity asc,
    rt.label asc;
$$;

La fonction utilise plusieurs règles simples :

  • La table doit appartenir au restaurant concerné et être active.
  • Sa capacité maximale doit être suffisante pour le groupe demandé.
  • Si le client demande explicitement une table extérieure, seules les tables marquées comme extérieures sont proposées.
  • Si le client demande une table accessible, seules les tables compatibles sont proposées.
  • Un créneau bloqué pour l’ensemble du restaurant rend les tables indisponibles.
  • Les réservations confirmées et les réservations temporairement bloquées occupent la table.
  • Une réservation temporaire expirée ne bloque plus la disponibilité.

La préférence terrasse est traitée ici comme une contrainte lorsqu’elle est demandée avec true. Dans de nombreux restaurants, la terrasse reste une préférence et non une garantie, notamment en cas de météo incertaine. Vous pourrez donc choisir, dans votre workflow n8n, de demander si cette préférence est impérative ou simplement souhaitée.

Testez la fonction de disponibilité

Après avoir remplacé l’identifiant de restaurant par une valeur réelle de votre projet de test, exécutez une requête similaire :

select *
from public.find_available_tables(
  'RESTAURANT_ID_A_REMPLACER',
  '2026-09-18 19:30:00+02',
  '2026-09-18 21:00:00+02',
  2,
  false,
  false
);

La requête doit retourner les tables actives qui peuvent accueillir deux personnes sur cette période. Si elle ne retourne aucune ligne, vérifiez les horaires, les données de table, les blocages éventuels et le fuseau horaire utilisé dans vos valeurs de test.

Créez des réservations temporaires avant confirmation

Une conversation WhatsApp peut prendre plusieurs minutes. Si un client demande une table, que le système propose un créneau, puis que le client répond plus tard, la disponibilité peut avoir changé entre-temps. Une réservation temporaire, appelée ici hold, permet de retenir brièvement une table pendant que le client confirme.

Cette réservation temporaire ne doit pas bloquer une table pendant des heures. Dans le tutoriel complet, vous utiliserez une durée courte et adaptée à votre flux, par exemple quelques minutes. La durée exacte dépend du nombre de demandes simultanées et de votre politique commerciale ; elle doit être décidée par le restaurant et testée en situation réelle.

Le statut hold exige une date d’expiration grâce à la contrainte définie dans la table reservations. Une fois cette date dépassée, la fonction find_available_tables considère que la réservation temporaire ne bloque plus la table.

Vous créerez plus tard une fonction de confirmation atomique. Elle devra créer la réservation temporaire, affecter la table et insérer l’événement correspondant de manière cohérente. À ce stade, retenez le principe : n8n ne doit pas créer une demande temporaire sans expiration, ni confirmer une réservation sans vérifier une dernière fois la disponibilité.

Ajoutez une fonction de nettoyage des réservations expirées

Les réservations temporaires expirées peuvent rester dans la base à des fins d’historique, mais leur statut doit être mis à jour. Cette opération facilite le suivi, les tableaux de bord et le débogage.

create or replace function public.expire_reservation_holds()
returns integer
language plpgsql
security definer
set search_path = public
as $$
declare
  updated_count integer;
begin
  update public.reservations
  set
    status = 'expired',
    updated_at = now(),
    updated_by = 'system:expire_reservation_holds'
  where status = 'hold'
    and hold_expires_at is not null
    and hold_expires_at <= now();

  get diagnostics updated_count = row_count;

  return updated_count;
end;
$$;

Vous pourrez appeler cette fonction depuis n8n à intervalle régulier ou planifier son exécution directement dans PostgreSQL si votre environnement et votre stratégie d’exploitation le permettent. Supabase documente notamment l’extension pg_cron pour planifier des tâches récurrentes avec une syntaxe cron. N’activez cette automatisation qu’après avoir testé le comportement des réservations temporaires et vérifié les droits d’exécution. [151]

Activez le Row Level Security avant d’exposer les tables

Le Row Level Security, souvent abrégé en RLS, permet de contrôler quelles lignes un utilisateur ou un rôle peut lire ou modifier. Cette protection est particulièrement importante si vous créez plus tard un tableau de bord, une interface de salle ou un portail multi-sites.

Dans une architecture simple où n8n est le seul processus qui accède directement à Supabase, vous pouvez commencer avec une base non exposée publiquement et des accès serveur limités. Dès que vous exposez des tables via une application ou que vous utilisez une clé publique dans un client, activez et testez vos politiques RLS.

alter table public.restaurants enable row level security;
alter table public.service_periods enable row level security;
alter table public.restaurant_tables enable row level security;
alter table public.blocked_slots enable row level security;
alter table public.customers enable row level security;
alter table public.reservations enable row level security;
alter table public.reservation_table_assignments enable row level security;
alter table public.reservation_events enable row level security;

Ne copiez pas une politique générique permissive dans une application réelle. Les règles dépendent de votre modèle d’authentification. Pour comprendre la logique, voici un exemple de politique de lecture réservée à un rôle authentifié. Elle ne constitue pas une politique finale : elle ne filtre pas encore par restaurant ou par rôle métier.

create policy "authenticated users can read restaurants"
on public.restaurants
for select
to authenticated
using (true);

Dans un projet multi-sites, une politique robuste devra lier l’utilisateur connecté à un ou plusieurs restaurant_id. Dans un projet mono-site, vous pourrez limiter l’accès au personnel authentifié et garder toutes les écritures critiques côté serveur.

Les fonctions qui modifient des réservations exigent une attention particulière. Si vous utilisez security definer, vérifiez le propriétaire de la fonction, fixez le search_path et accordez uniquement les droits nécessaires. Ces détails seront repris dans la partie consacrée aux workflows et aux appels sécurisés depuis n8n.

Sécurité : le Row Level Security ne protège pas une clé de service exposée. Une clé privilégiée contourne généralement les politiques prévues pour les utilisateurs ordinaires. Conservez-la uniquement dans un environnement serveur contrôlé et limitez les personnes qui peuvent y accéder.

Testez les scénarios essentiels avant de connecter n8n

Avant d’ajouter WhatsApp ou un modèle d’IA, vérifiez le comportement de la base avec des données fictives. Le but est d’identifier les incohérences de modèle avant qu’un client réel ne soit concerné.

Testez au minimum les scénarios suivants :

  • Créer une réservation de démonstration pour deux personnes sur une table de deux.
  • Affecter cette réservation à une table disponible avec une plage horaire explicite.
  • Tenter de créer une seconde affectation sur la même table avec un créneau qui se chevauche.
  • Vérifier que PostgreSQL refuse la seconde affectation.
  • Créer une réservation temporaire avec une date d’expiration dans le passé et vérifier qu’elle ne bloque plus la fonction de disponibilité.
  • Ajouter une fermeture exceptionnelle dans blocked_slots et vérifier qu’aucune table n’est proposée sur cette période.
  • Demander une table extérieure et vérifier que seules les tables concernées sont retournées.
  • Demander une table accessible et vérifier que le filtre fonctionne selon vos données.
  • Vérifier qu’une réservation annulée ne bloque plus une table lorsque l’affectation est supprimée ou que votre logique de disponibilité l’exclut.
  • Contrôler qu’aucune clé de service ou secret n’apparaît dans les journaux, captures, exports ou messages de test.

Voici un exemple de création de réservation fictive. Vous devrez d’abord créer un client, une réservation, puis une affectation de table. L’exemple est volontairement séparé en plusieurs étapes afin de rendre les liens entre les tables visibles.

insert into public.customers (
  restaurant_id,
  whatsapp_phone,
  full_name,
  language_code
)
values (
  'RESTAURANT_ID_A_REMPLACER',
  '+33600000000',
  'Client de test',
  'fr'
)
returning id;
insert into public.reservations (
  restaurant_id,
  customer_id,
  status,
  source,
  party_size,
  starts_at,
  ends_at,
  customer_name,
  customer_phone,
  language_code,
  created_by,
  updated_by
)
values (
  'RESTAURANT_ID_A_REMPLACER',
  'CUSTOMER_ID_A_REMPLACER',
  'confirmed',
  'manual',
  2,
  '2026-09-18 19:30:00+02',
  '2026-09-18 21:00:00+02',
  'Client de test',
  '+33600000000',
  'fr',
  'manual:test',
  'manual:test'
)
returning id;
insert into public.reservation_table_assignments (
  reservation_id,
  restaurant_table_id,
  starts_at,
  ends_at
)
values (
  'RESERVATION_ID_A_REMPLACER',
  'TABLE_ID_A_REMPLACER',
  '2026-09-18 19:30:00+02',
  '2026-09-18 21:00:00+02'
);

Si vous répétez la dernière insertion avec la même table et une plage qui se chevauche, PostgreSQL doit retourner une erreur liée à la contrainte d’exclusion. Cette erreur est attendue : elle prouve que la protection contre le double créneau fonctionne.

Ce que vous avez construit et ce qui vient ensuite

Vous disposez maintenant d’une fondation relationnelle pour gérer les réservations. Vous avez créé des établissements, des services, des tables, des blocages, des clients, des réservations, des affectations de tables et un journal d’événements. Vous avez également ajouté une protection contre les chevauchements et une fonction permettant de rechercher des tables disponibles.

Dans la partie suivante, vous allez connecter cette base à WhatsApp Business Cloud API et à n8n. Vous configurerez le webhook, recevrez un message entrant, normaliserez les données et préparerez un état de conversation. Vous n’enverrez pas encore de confirmation automatique : l’objectif sera d’abord de recevoir, comprendre et tracer une demande de façon fiable.

Explorer les analyses sur l’IA et l’e-commerce

Sources et pour aller plus loin

Flux de message WhatsApp entrant vers n8n puis Supabase pour traiter une demande de réservation restaurant
Flux de message WhatsApp entrant vers n8n puis Supabase pour traiter une demande de réservation restaurant

Partie 3 : connecter WhatsApp à n8n et Supabase pour recevoir les demandes de réservation

Votre base Supabase peut désormais représenter les services, les tables, les créneaux, les réservations et les règles de disponibilité. Vous allez maintenant créer le point d’entrée du système : un client écrit sur WhatsApp, Meta transmet l’événement à n8n, puis n8n enregistre les informations nécessaires dans Supabase.

À ce stade, votre assistant ne confirme pas de réservation. Il ne répond pas encore à une demande d’allergie, ne choisit pas une table et ne génère pas de proposition de créneau. Son rôle est plus simple, mais indispensable : recevoir un message de manière fiable, identifier le client, conserver le contexte utile et préparer la suite du workflow.

À retenir : votre webhook WhatsApp doit traiter deux flux distincts. Meta utilise une requête GET pour vérifier que votre endpoint vous appartient, puis envoie les messages et statuts en POST. Dans n8n, séparez clairement ces deux traitements afin d’éviter les erreurs de validation et les réponses imprévues.

Comprendre le parcours entre WhatsApp, Meta, n8n et Supabase

WhatsApp Business Cloud API ne transmet pas les messages directement à votre base de données. Meta appelle une URL publique, appelée webhook, que vous configurez dans votre application Meta. Cette URL reçoit des données JSON décrivant l’événement : message entrant, statut d’envoi, statut de livraison ou statut de lecture.

Dans cette architecture, n8n devient le point de réception. Il reçoit le webhook, vérifie qu’il s’agit d’un événement utile, extrait les données importantes, puis appelle Supabase pour enregistrer ou mettre à jour les données.

Client WhatsApp
        ↓
WhatsApp Business Cloud API
        ↓
Webhook public n8n
        ↓
Vérification et normalisation
        ↓
Recherche ou création du client Supabase
        ↓
Enregistrement du message et de l’état de conversation
        ↓
Préparation de la classification et de la réservation

Vous devez conserver cette séparation. WhatsApp est le canal de communication. n8n orchestre les actions. Supabase conserve les données et les règles. Le modèle d’intelligence artificielle, que vous ajouterez dans la partie suivante, interprétera les messages sans devenir la source de vérité de vos disponibilités.

Préparez les identifiants et les secrets avant de créer le webhook

Vous aurez besoin de plusieurs informations pour connecter WhatsApp à n8n. Ne copiez jamais ces valeurs dans un document public, un article WordPress, un message client ou un nœud n8n partagé sans contrôle. Enregistrez-les dans les identifiants sécurisés n8n ou dans des variables d’environnement protégées.

  • Le jeton d’accès WhatsApp Cloud API fourni par Meta.
  • L’identifiant du numéro de téléphone WhatsApp Business utilisé pour envoyer les messages.
  • L’identifiant du compte WhatsApp Business, lorsque votre configuration le demande.
  • Un jeton de vérification de webhook choisi par vous-même.
  • L’URL publique de production de votre instance n8n.
  • L’URL et les identifiants d’accès à votre projet Supabase.
  • Les identifiants du restaurant de test créés dans la partie précédente.

Le jeton de vérification de webhook n’est pas le même élément que le jeton d’accès WhatsApp. Le premier sert à prouver à Meta que l’URL webhook est bien contrôlée par votre organisation. Le second sert à authentifier les appels sortants vers l’API WhatsApp lorsque vous envoyez un message.

Point de vigilance : un token WhatsApp ou une clé privilégiée Supabase exposés peuvent permettre à un tiers d’envoyer des messages, de consulter des données ou d’endommager votre environnement. Utilisez des identifiants n8n, limitez les accès administrateur et renouvelez immédiatement un secret qui aurait été partagé par erreur.

Utilisez l’URL de production n8n pour Meta

Le nœud Webhook de n8n propose une URL de test et une URL de production. Pendant la construction d’un workflow, l’URL de test est utile pour inspecter les données envoyées et corriger la configuration. Elle n’est pas adaptée à une intégration WhatsApp durable.

Lorsque vous configurez Meta, utilisez l’URL de production du workflow activé. Vérifiez que votre instance n8n est accessible depuis Internet avec une connexion HTTPS valide. Si n8n est auto-hébergé, un domaine, un certificat TLS et une configuration de reverse proxy peuvent être nécessaires.

Complétez votre schéma Supabase avec les conversations et les messages

La partie 2 a créé les tables liées aux réservations. Vous devez maintenant ajouter une table de conversation et une table de messages. Elles permettent d’éviter de poser la même question plusieurs fois, de détecter les messages déjà traités et de comprendre ce qui s’est passé lorsqu’un client signale un problème.

Dans Supabase, ouvrez l’éditeur SQL et créez un nouveau script, par exemple 02_conversations_whatsapp.sql. Exécutez le script suivant dans votre environnement de développement.

create type public.conversation_status as enum (
  'active',
  'waiting_for_customer',
  'waiting_for_human',
  'closed',
  'expired'
);

create type public.message_direction as enum (
  'inbound',
  'outbound'
);

create type public.message_type as enum (
  'text',
  'image',
  'document',
  'audio',
  'video',
  'interactive',
  'button',
  'unknown'
);

create table public.conversations (
  id uuid primary key default gen_random_uuid(),
  restaurant_id uuid not null references public.restaurants(id) on delete cascade,
  customer_id uuid not null references public.customers(id) on delete cascade,
  whatsapp_conversation_id text,
  status public.conversation_status not null default 'active',
  current_step text not null default 'new_message',
  context jsonb not null default '{}'::jsonb,
  last_message_at timestamptz,
  expires_at timestamptz,
  assigned_to text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (restaurant_id, customer_id)
);

create table public.messages (
  id uuid primary key default gen_random_uuid(),
  restaurant_id uuid not null references public.restaurants(id) on delete cascade,
  customer_id uuid references public.customers(id) on delete set null,
  conversation_id uuid references public.conversations(id) on delete set null,
  whatsapp_message_id text,
  direction public.message_direction not null,
  message_type public.message_type not null default 'unknown',
  message_text text,
  raw_payload jsonb not null default '{}'::jsonb,
  processed_at timestamptz,
  processing_error text,
  created_at timestamptz not null default now(),
  unique (whatsapp_message_id)
);

create index conversations_restaurant_status_idx
  on public.conversations (restaurant_id, status);

create index conversations_customer_id_idx
  on public.conversations (customer_id);

create index messages_conversation_created_at_idx
  on public.messages (conversation_id, created_at desc);

create index messages_customer_created_at_idx
  on public.messages (customer_id, created_at desc);

La table conversations ne stocke pas chaque phrase reçue. Elle conserve l’état du parcours : par exemple, le client a déjà donné le nombre de personnes, mais il manque la date. La colonne context peut contenir des données structurées limitées, comme une date demandée, un nombre de couverts ou une préférence de placement.

La table messages sert de journal technique et opérationnel. L’identifiant de message WhatsApp est unique. Cette contrainte est importante : si Meta ou votre infrastructure transmet à nouveau un événement, vous évitez de traiter deux fois la même demande et d’envoyer plusieurs réponses identiques.

Ajoutez une fonction de mise à jour automatique

Les tables de votre projet contiennent plusieurs colonnes updated_at. Pour éviter de les mettre à jour manuellement dans chaque workflow, vous pouvez créer une fonction PostgreSQL et l’associer à des triggers.

create or replace function public.set_updated_at()
returns trigger
language plpgsql
as $$
begin
  new.updated_at = now();
  return new;
end;
$$;

create trigger set_conversations_updated_at
before update on public.conversations
for each row
execute function public.set_updated_at();

create trigger set_customers_updated_at
before update on public.customers
for each row
execute function public.set_updated_at();

create trigger set_reservations_updated_at
before update on public.reservations
for each row
execute function public.set_updated_at();

Si vous avez déjà créé un trigger semblable dans votre environnement, ne le dupliquez pas. Vérifiez d’abord les fonctions et triggers existants dans votre projet Supabase.

Créez le workflow n8n de vérification du webhook Meta

Meta vérifie l’URL webhook avant de commencer à transmettre des événements. Cette vérification utilise une requête GET avec des paramètres de requête. Votre workflow doit vérifier que le jeton reçu correspond à votre jeton secret, puis renvoyer la valeur du challenge fournie par Meta.

Dans n8n, créez un nouveau workflow nommé WA-01 — Vérification webhook Meta. Utilisez le chemin webhook de votre choix, par exemple whatsapp-reservations. L’URL complète dépendra de votre domaine n8n.

  1. Ajoutez un nœud Webhook.
  2. Choisissez la méthode HTTP GET.
  3. Définissez un chemin unique, par exemple whatsapp-reservations.
  4. Dans le paramètre de réponse du Webhook, sélectionnez une réponse via le nœud Respond to Webhook.
  5. Ajoutez un nœud IF pour comparer le paramètre hub.verify_token reçu avec votre jeton de vérification stocké de façon sécurisée.
  6. Dans la branche valide, ajoutez un nœud Respond to Webhook qui renvoie la valeur de hub.challenge avec un code HTTP 200.
  7. Dans la branche invalide, ajoutez un second nœud Respond to Webhook qui renvoie un code HTTP 403.

La logique recherchée est la suivante :

GET /webhook/whatsapp-reservations
        ↓
Lire hub.mode, hub.verify_token et hub.challenge
        ↓
Le jeton correspond-il au secret enregistré ?
        ↓
Oui : renvoyer hub.challenge avec HTTP 200
Non : renvoyer HTTP 403

Selon la structure affichée par votre nœud Webhook, les paramètres peuvent être disponibles dans $json.query ou dans une autre propriété équivalente. Avant de construire votre expression, lancez le mode test et inspectez le JSON réel reçu par n8n. Ne recopiez pas une expression sans vérifier le nom des champs dans votre version.

Une expression indicative de comparaison peut ressembler à celle-ci :

{{ $json.query["hub.verify_token"] === $env.WHATSAPP_WEBHOOK_VERIFY_TOKEN }}

Cette expression constitue un exemple. Si votre instance n8n n’expose pas vos variables de cette manière, utilisez un identifiant, un coffre de secrets ou la méthode recommandée par votre configuration. L’objectif est de ne pas inscrire le jeton en clair dans un nœud qui pourrait être exporté ou partagé.

Configurez Meta pour valider le webhook

Dans votre application Meta associée à WhatsApp Business Cloud API, renseignez l’URL de production du workflow n8n et le même jeton de vérification que celui attendu par votre workflow. Abonnez ensuite le webhook aux événements nécessaires, notamment ceux relatifs aux messages.

Meta doit recevoir la réponse au challenge lors de la validation. Si la validation échoue, vérifiez en priorité :

  • Que le workflow n8n est activé.
  • Que vous utilisez l’URL de production, et non l’URL de test.
  • Que l’URL est accessible publiquement en HTTPS.
  • Que le chemin configuré dans Meta correspond exactement au chemin du Webhook n8n.
  • Que le jeton de vérification est identique des deux côtés.
  • Que votre workflow renvoie bien le challenge avec un code HTTP 200 lorsque le jeton est valide.

Une fois le webhook validé, ne modifiez pas son chemin ou son jeton sans mettre à jour Meta. Sinon, les messages entrants ne pourront plus être transmis correctement.

Créez le workflow n8n de réception des événements WhatsApp

Créez un second workflow nommé WA-02 — Réception des messages WhatsApp. Il peut utiliser le même chemin webhook, mais avec la méthode HTTP POST. n8n différencie les webhooks notamment par leur méthode HTTP ; vérifiez cependant la configuration finale dans votre environnement avant de la déployer.

Ce workflow reçoit plusieurs types d’événements. Tous ne représentent pas un nouveau message client. Meta peut également envoyer des statuts techniques, par exemple des informations relatives à l’envoi, à la livraison ou à la lecture d’un message sortant.

Votre premier objectif consiste donc à filtrer les événements. Ne lancez pas une logique de réservation sur chaque payload reçu.

  1. Ajoutez un nœud Webhook en méthode POST.
  2. Configurez une réponse rapide avec un nœud Respond to Webhook qui renvoie un statut HTTP 200.
  3. Ajoutez un nœud Code ou Set pour extraire les objets utiles du payload.
  4. Ajoutez un nœud IF ou Switch pour distinguer les messages entrants des statuts techniques.
  5. Pour un message entrant, normalisez le téléphone, l’identifiant du message, le type de contenu, le texte et l’horodatage.
  6. Pour un statut technique, enregistrez éventuellement l’information dans un flux séparé, mais ne déclenchez pas la logique de réservation.

Répondez rapidement au webhook après avoir reçu l’événement. Vous pouvez utiliser le nœud Respond to Webhook pour contrôler le moment et la forme de la réponse. La suite du workflow peut ensuite poursuivre le traitement dans n8n.

Extrayez un message texte de manière défensive

Un payload WhatsApp réel contient plusieurs niveaux. La structure exacte doit être vérifiée dans vos premières exécutions. L’objectif est d’extraire uniquement les informations utiles à la réservation sans présumer que chaque événement contient du texte.

Dans un nœud Code n8n, vous pouvez adopter une logique de normalisation comparable à cet exemple indicatif :

const entry = $json.entry?.[0];
const change = entry?.changes?.[0];
const value = change?.value;

const message = value?.messages?.[0];
const contact = value?.contacts?.[0];

if (!message) {
  return [];
}

const messageType = message.type ?? 'unknown';
const messageText =
  messageType === 'text'
    ? message.text?.body ?? ''
    : '';

return [
  {
    json: {
      whatsapp_message_id: message.id,
      whatsapp_phone: message.from,
      profile_name: contact?.profile?.name ?? null,
      message_type: messageType,
      message_text: messageText,
      received_at: message.timestamp
        ? new Date(Number(message.timestamp) * 1000).toISOString()
        : new Date().toISOString(),
      raw_payload: $json
    }
  }
];

Ce code est un modèle de départ, pas une garantie universelle. Votre payload peut contenir plusieurs entrées, plusieurs changements ou plusieurs messages. Avant une mise en production, adaptez la logique pour traiter tous les éléments pertinents et ne pas ignorer silencieusement un message valide.

Le champ whatsapp_phone doit être normalisé au format international. Selon les données reçues, il peut ne pas contenir le préfixe +. Définissez une convention unique dans votre base. Dans ce tutoriel, la colonne whatsapp_phone utilise un format international cohérent, par exemple +33600000000.

Créez ou retrouvez le client dans Supabase

Après normalisation, n8n doit retrouver le client correspondant au numéro WhatsApp. Si ce numéro n’existe pas encore dans la table customers, le workflow crée une fiche minimale. Si le client existe déjà, le workflow peut mettre à jour son nom lorsqu’un nom de profil est disponible et qu’aucune information plus fiable n’est déjà stockée.

Pour cette opération, vous pouvez utiliser le nœud Supabase de n8n si votre version le propose et si ses opérations correspondent à votre besoin. Vous pouvez aussi appeler l’API REST Supabase avec un nœud HTTP Request. Dans les deux cas, utilisez des identifiants sécurisés et limitez les colonnes consultées ou modifiées.

La logique fonctionnelle est la suivante :

Rechercher customers
où restaurant_id = restaurant courant
et whatsapp_phone = téléphone normalisé
        ↓
Client trouvé ?
        ↓
Oui : utiliser customer_id existant
Non : créer une fiche minimale
        ↓
Mettre à jour le nom seulement si nécessaire
        ↓
Transmettre customer_id à la suite du workflow

Dans une version mono-site, le restaurant_id peut être défini dans les variables d’environnement n8n. Dans un environnement multi-sites, il devra être déterminé à partir du numéro WhatsApp destinataire, de l’identifiant du compte ou d’une table de correspondance. Ne choisissez pas un restaurant au hasard à partir du contenu du message.

Le client ne doit pas être inscrit automatiquement à une campagne marketing parce qu’il a demandé une réservation. La colonne marketing_consent créée dans la partie 2 reste à false tant qu’un consentement explicite, conforme à vos règles et à votre contexte, n’a pas été recueilli.

Enregistrez le message et empêchez les doublons

Une fois le client identifié, insérez un enregistrement dans la table messages. L’identifiant unique de message WhatsApp doit être conservé. Il permet au workflow de détecter les retransmissions éventuelles et d’éviter de créer plusieurs fois la même demande de réservation.

Vous pouvez utiliser une fonction PostgreSQL pour centraliser cette insertion. Ajoutez le script suivant dans Supabase :

create or replace function public.record_inbound_whatsapp_message(
  p_restaurant_id uuid,
  p_customer_id uuid,
  p_conversation_id uuid,
  p_whatsapp_message_id text,
  p_message_type public.message_type,
  p_message_text text,
  p_raw_payload jsonb,
  p_received_at timestamptz
)
returns uuid
language plpgsql
security definer
set search_path = public
as $$
declare
  v_message_id uuid;
begin
  insert into public.messages (
    restaurant_id,
    customer_id,
    conversation_id,
    whatsapp_message_id,
    direction,
    message_type,
    message_text,
    raw_payload,
    processed_at,
    created_at
  )
  values (
    p_restaurant_id,
    p_customer_id,
    p_conversation_id,
    p_whatsapp_message_id,
    'inbound',
    p_message_type,
    p_message_text,
    p_raw_payload,
    now(),
    p_received_at
  )
  on conflict (whatsapp_message_id)
  do update set processed_at = public.messages.processed_at
  returning id into v_message_id;

  return v_message_id;
end;
$$;

Cette fonction utilise on conflict sur l’identifiant de message. Si Meta transmet à nouveau le même événement, l’insertion ne crée pas une seconde ligne. Vous devez toutefois décider, dans n8n, si un message déjà enregistré doit arrêter complètement le workflow. Dans la plupart des cas, la réponse est oui : un message dupliqué ne doit pas générer une seconde proposition de réservation.

Créez et mettez à jour l’état de conversation

Une réservation se déroule rarement en un seul message. Le client peut d’abord demander une table, puis indiquer le nombre de couverts, préciser la date et confirmer une proposition plusieurs minutes plus tard. Sans état de conversation, le workflow risque d’interpréter chaque message comme une nouvelle demande.

La table conversations vous permet de conserver l’étape en cours et un contexte réduit. Voici une fonction qui crée ou met à jour la conversation active d’un client :

create or replace function public.upsert_active_conversation(
  p_restaurant_id uuid,
  p_customer_id uuid,
  p_last_message_at timestamptz
)
returns uuid
language plpgsql
security definer
set search_path = public
as $$
declare
  v_conversation_id uuid;
begin
  insert into public.conversations (
    restaurant_id,
    customer_id,
    status,
    current_step,
    last_message_at,
    expires_at
  )
  values (
    p_restaurant_id,
    p_customer_id,
    'active',
    'new_message',
    p_last_message_at,
    now() + interval '24 hours'
  )
  on conflict (restaurant_id, customer_id)
  do update set
    status = case
      when public.conversations.status in ('closed', 'expired')
        then 'active'::public.conversation_status
      else public.conversations.status
    end,
    last_message_at = excluded.last_message_at,
    expires_at = now() + interval '24 hours'
  returning id into v_conversation_id;

  return v_conversation_id;
end;
$$;

La durée de vingt-quatre heures utilisée dans cet exemple est un choix technique indicatif pour expirer un contexte de conversation qui n’est plus actif. Elle ne doit pas être confondue avec les règles Meta applicables à l’envoi de messages. Adaptez la durée de conservation du contexte à votre parcours, à vos obligations et à vos règles internes.

La colonne context pourra contenir une structure réduite, par exemple :

{
  "intent": "reservation",
  "party_size": 4,
  "requested_date": "2026-09-18",
  "requested_time": "20:00",
  "seating_preference": "terrace_preferred",
  "missing_fields": ["customer_name"],
  "requires_human_review": false
}

Dans la partie 4, l’agent IA ne recevra pas carte blanche pour modifier cette structure. Il devra produire une sortie structurée validée. n8n vérifiera cette sortie avant de mettre à jour le contexte ou de consulter la disponibilité.

Préparez un premier message sortant de test

Avant de créer un assistant de réservation complet, vérifiez que votre système peut envoyer un message texte contrôlé. WhatsApp Cloud API utilise un endpoint de type POST /<WHATSAPP_BUSINESS_PHONE_NUMBER_ID>/messages pour envoyer un message texte à un utilisateur.

Dans n8n, créez un nœud HTTP Request distinct, avec les paramètres suivants :

  • Method : POST.
  • URL : l’endpoint Meta correspondant à votre version d’API et à votre identifiant de numéro de téléphone WhatsApp Business.
  • Authentication : un jeton Bearer stocké dans un identifiant n8n sécurisé.
  • Content-Type : application/json.
  • Body : un message texte de test destiné à un numéro autorisé dans votre environnement de test.

Voici un corps de requête indicatif pour un message texte. Remplacez les valeurs uniquement dans votre configuration n8n sécurisée.

{
  "messaging_product": "whatsapp",
  "recipient_type": "individual",
  "to": "NUMERO_DESTINATAIRE_EN_FORMAT_INTERNATIONAL",
  "type": "text",
  "text": {
    "preview_url": false,
    "body": "Message de test : votre assistant de réservation est en cours de configuration."
  }
}

Ne connectez pas encore ce nœud à tous les messages entrants. Testez-le manuellement sur un numéro autorisé. Vérifiez ensuite les statuts de livraison reçus dans les webhooks, afin de distinguer un message accepté par l’API d’un message effectivement livré ou lu.

Les messages envoyés en dehors des conditions autorisées par WhatsApp peuvent exiger l’utilisation d’un modèle de message approuvé. Avant de créer des rappels, des relances ou des campagnes, vérifiez les règles applicables à votre compte, à la catégorie de message et au moment de l’envoi.

Construisez un traitement robuste des erreurs

Un assistant de réservation ne doit pas échouer silencieusement. Si Supabase est indisponible, si un token Meta a expiré ou si le payload reçu ne contient pas les informations attendues, n8n doit créer une trace exploitable et prévenir une personne responsable.

Dans la version pilote, prévoyez au minimum trois niveaux de traitement :

Type de problème Exemple Réaction recommandée
Événement sans message client Statut de livraison d’un message sortant Ignorer dans le flux de réservation ou journaliser dans un flux technique séparé
Donnée incomplète Message entrant sans texte exploitable Enregistrer l’événement et prévoir une réponse ou une reprise humaine adaptée
Erreur technique temporaire Échec d’appel à Supabase ou à Meta Réessayer avec une limite, journaliser l’erreur et prévenir l’administrateur
Erreur de logique métier Client associé à plusieurs conversations actives Stopper l’automatisation et transférer le cas à une file de contrôle
Secret ou accès invalide Erreur 401 ou 403 sur un appel API Ne pas réessayer indéfiniment ; alerter le responsable technique pour vérifier les identifiants

Les erreurs techniques ne doivent pas être envoyées telles quelles au client. Un message interne peut indiquer qu’une demande doit être reprise manuellement, mais évitez de dévoiler des informations sur vos outils, vos clés, votre infrastructure ou la structure de votre base de données.

Testez le parcours avant de connecter l’intelligence artificielle

Vous pouvez maintenant tester un flux entrant complet, sans encore activer la qualification par IA. Utilisez un numéro de test, des données fictives et un restaurant de développement.

  • Vérifiez que Meta valide le webhook n8n avec une requête GET.
  • Envoyez un message texte depuis un numéro autorisé vers votre numéro WhatsApp Business de test.
  • Contrôlez que le webhook POST n8n reçoit le payload attendu.
  • Vérifiez que le workflow ignore les événements qui ne contiennent pas de message entrant.
  • Contrôlez le format du numéro de téléphone enregistré dans Supabase.
  • Vérifiez qu’un client est créé une seule fois pour le même numéro et le même restaurant.
  • Vérifiez qu’un message entrant est enregistré avec un identifiant WhatsApp unique.
  • Envoyez de nouveau le même événement de test si possible et vérifiez qu’aucun doublon n’est créé.
  • Contrôlez qu’une conversation active est créée ou mise à jour.
  • Testez l’envoi manuel d’un message texte depuis n8n vers un numéro autorisé.
  • Simulez une erreur d’identifiant ou d’accès et vérifiez que l’équipe reçoit une alerte interne plutôt qu’un message d’erreur technique envoyé au client.

Lorsque ces tests fonctionnent, vous disposez d’un socle solide. Vous pouvez alors ajouter la logique conversationnelle, la classification des intentions et la vérification de disponibilité sans mélanger les problèmes de réception de messages avec les problèmes de règles métier.

Conclusion de la partie 3

Vous avez maintenant relié WhatsApp, n8n et Supabase de manière structurée. Le système sait recevoir un message entrant, différencier les étapes de vérification et les événements de messagerie, reconnaître un client, éviter les doublons de messages et créer un contexte de conversation.

Dans la prochaine partie, vous allez utiliser ce contexte pour construire un agent IA encadré. Il devra reconnaître une demande de réservation, de modification, d’annulation ou de question fréquente ; extraire les informations utiles dans un format structuré ; puis transmettre les données à des fonctions Supabase qui vérifieront réellement la disponibilité. L’IA préparera la décision, mais les règles métier continueront de décider.

Explorer les analyses sur les chatbots et agents IA

Sources et pour aller plus loin

L’IA interprète le message, tandis que la base applique les règles de réservation et que l’équipe traite les cas non automatisables.
L’IA interprète le message, tandis que la base applique les règles de réservation et que l’équipe traite les cas non automatisables.

Partie 4 : créer un agent IA de réservation restaurant sans lui laisser décider seul

Votre système peut maintenant recevoir un message WhatsApp, identifier un client, enregistrer les messages et conserver un état de conversation dans Supabase. Vous allez ajouter l’intelligence artificielle pour comprendre ce que le client demande, extraire les informations utiles et orienter le workflow.

Cette étape ne consiste pas à laisser un modèle de langage gérer seul les réservations. Un modèle peut interpréter un message écrit de façon naturelle, mais il peut aussi mal comprendre une date, ignorer une nuance ou produire une réponse convaincante alors que la disponibilité n’a pas été vérifiée. Dans ce tutoriel, l’IA prépare la décision. Supabase et les règles métier prennent la décision. L’équipe du restaurant traite les exceptions.

À retenir : utilisez l’IA pour transformer un message libre en données structurées, pas pour inventer une réponse opérationnelle. Une disponibilité, une allergie, un changement de table ou une annulation doivent toujours être vérifiés par les règles métier et la base de données avant qu’un message définitif soit envoyé au client.

Pourquoi l’agent IA ne doit jamais confirmer une réservation seul

Un client peut formuler une demande de multiples façons : « Une table demain soir ? », « Vous avez encore de la place vers 21 heures pour trois ? », « Je voudrais modifier notre réservation de vendredi » ou « Mon fils est allergique aux arachides, est-ce que vous pouvez l’accueillir ? ».

Le rôle de l’IA est utile à ce stade, car elle peut reconnaître l’intention et extraire une information à partir d’un texte libre. Elle peut identifier une date, un horaire approximatif, un nombre de personnes, une préférence de terrasse ou une demande de modification.

Elle ne doit toutefois jamais produire une confirmation définitive à partir de cette seule compréhension. Elle ne connaît pas automatiquement :

  • Les tables déjà réservées et les réservations temporaires actives.
  • Les fermetures exceptionnelles et les réductions de capacité.
  • Les règles particulières selon le jour, le service ou la taille du groupe.
  • Les changements effectués manuellement par l’équipe entre deux messages.
  • Les capacités réelles de la cuisine à gérer une allergie ou une demande spécifique.
  • Les restrictions applicables aux annulations, aux acomptes ou aux événements.

Une architecture sûre doit donc respecter la séquence suivante :

Message WhatsApp du client
        ↓
IA : intention et données structurées
        ↓
n8n : validation des champs et règles de routage
        ↓
Supabase : disponibilité, contraintes et opérations atomiques
        ↓
n8n : construction d’une réponse fondée sur le résultat réel
        ↓
WhatsApp : proposition, confirmation ou transfert humain

Définissez un contrat de sortie JSON pour l’agent

La sortie de l’agent doit être exploitable par n8n sans qu’un workflow tente d’interpréter des phrases libres. Au lieu de demander au modèle de répondre « Je pense que le client veut une réservation demain », demandez-lui de retourner des champs déterminés.

Les technologies de sortie structurée permettent d’imposer un schéma JSON. Cette méthode réduit les erreurs de format et empêche par exemple le modèle d’inventer une intention non prévue par votre workflow. Elle ne dispense pas de vérifier les valeurs extraites : une date peut être syntaxiquement valide tout en étant mal interprétée.

Pour ce tutoriel, utilisez les intentions suivantes :

Intention Quand l’utiliser Action autorisée ensuite
new_reservation Le client souhaite réserver une nouvelle table Collecter les champs, puis vérifier la disponibilité
availability_request Le client demande simplement si un créneau est disponible Demander les informations manquantes ou rechercher les créneaux
confirm_proposal Le client confirme un créneau proposé par le système Vérifier une dernière fois et confirmer si les règles le permettent
modify_reservation Le client souhaite changer un horaire, une date ou un nombre de couverts Identifier la réservation, libérer ou retenir le nouveau créneau selon le workflow
cancel_reservation Le client souhaite annuler Identifier la réservation et appliquer la politique définie
faq Le client pose une question sur les horaires, l’adresse ou les conditions générales Répondre uniquement à partir d’une base de réponses validées
human_handoff Le message exige une vérification humaine Créer une demande dans la file d’intervention
unknown L’intention ne peut pas être comprise avec assez de certitude Demander une précision ou transférer selon le contexte

Ajoutez également un niveau de risque. Il sert à empêcher qu’un message contenant une allergie grave, une plainte ou une demande de privatisation poursuive automatiquement vers la réservation, même si le modèle a également identifié une date et un nombre de couverts.

{
  "intent": "new_reservation",
  "confidence": "high",
  "risk_level": "normal",
  "requires_human_review": false,
  "reason_for_handoff": null,
  "party_size": 4,
  "requested_date": "2026-09-18",
  "requested_time": "20:00",
  "time_is_flexible": false,
  "customer_name": null,
  "seating_preference": "terrace_preferred",
  "accessibility_needed": false,
  "allergy_or_medical_request": false,
  "special_request": null,
  "reservation_reference": null,
  "missing_fields": ["customer_name"],
  "suggested_customer_question": "Pour finaliser votre demande, quel nom devons-nous indiquer pour la réservation ?"
}

Dans ce schéma, le modèle n’a pas le droit de renvoyer un champ du type reservation_confirmed, available_table ou allergy_supported. Ces décisions appartiennent au workflow et à la base de données.

Écrivez un prompt système limité et explicite

Le prompt système ne doit pas demander à l’IA d’être « un excellent assistant de restaurant » sans cadre précis. Il doit définir ce qu’elle peut faire, ce qu’elle ne peut pas faire, les intentions autorisées, les règles de prudence et le format de sortie attendu.

Dans n8n, vous pouvez utiliser un nœud AI Agent avec un analyseur de sortie structurée, ou appeler directement un fournisseur de modèle qui prend en charge les sorties structurées. Le choix dépend de votre version n8n, de votre fournisseur, de vos contraintes de données et de votre architecture. Quelle que soit l’option choisie, conservez le même contrat JSON et validez sa sortie avant toute action.

Voici un prompt système de départ, à adapter avec vos propres règles restaurant :

Vous êtes un analyseur de messages pour un assistant de réservation de restaurant.

Votre mission est uniquement de :
1. classifier l’intention du client ;
2. extraire les informations explicitement présentes dans le message ;
3. détecter les demandes nécessitant une intervention humaine ;
4. retourner exclusivement un objet conforme au schéma JSON fourni.

Vous ne confirmez jamais une réservation.
Vous ne déclarez jamais qu’un créneau est disponible.
Vous ne promettez jamais une table, une terrasse, une préférence de placement, un menu ou une prestation.
Vous ne confirmez jamais la prise en charge d’une allergie, d’un régime médical ou d’une demande de santé.
Vous n’inventez jamais de date, d’heure, de nombre de personnes, de coordonnées, de politique ou de référence de réservation.
Si une information est ambiguë, absente ou contradictoire, ajoutez-la à missing_fields ou choisissez l’intention unknown.
Si le message mentionne une allergie grave, anaphylaxie, urgence, groupe important, privatisation, devis, plainte, litige, facture, paiement, harcèlement ou menace, définissez requires_human_review à true et choisissez human_handoff lorsque cela est nécessaire.
Utilisez uniquement les intentions prévues par le schéma.
Retournez uniquement le JSON structuré, sans phrase autour.

Le prompt doit recevoir le contexte nécessaire, mais pas davantage. Transmettez par exemple le dernier message, l’étape de conversation actuelle, la langue connue du client et les éléments déjà confirmés. Évitez d’envoyer une longue archive de messages ou des données personnelles qui ne sont pas utiles à l’interprétation.

Construisez le workflow n8n de classification et d’extraction

Créez un workflow nommé WA-03 — Classification et extraction IA. Il reçoit les données normalisées du workflow de réception construit dans la partie 3, puis retourne une sortie structurée que n8n peut utiliser.

Le workflow doit suivre une progression simple :

  1. Recevoir le message normalisé, le client et la conversation active.
  2. Vérifier que le message est de type texte et qu’il contient un contenu exploitable.
  3. Récupérer le contexte de conversation limité depuis Supabase.
  4. Envoyer le message et le contexte au modèle avec le prompt système.
  5. Valider que la sortie respecte le schéma attendu.
  6. Vérifier les règles de sécurité indépendamment de l’IA, par exemple avec une liste de mots-clés sensibles.
  7. Router le résultat vers une branche de réservation, de modification, d’annulation, de FAQ ou de transfert humain.

Dans n8n, activez une sortie structurée lorsque votre modèle et votre nœud le permettent. Le nœud Tools Agent de n8n peut imposer un format de sortie et utiliser notamment un Structured Output Parser. Vous pouvez également demander une validation humaine avant l’exécution d’outils sensibles.

Ajoutez un nœud Switch après la sortie structurée. Il doit orienter le workflow selon la valeur intent. Ne créez pas un seul long workflow avec des conditions dispersées. Des branches séparées rendent les erreurs plus visibles et facilitent les tests.

Sortie IA structurée
        ↓
Contrôle de sécurité n8n
        ↓
Switch sur intent
        ├── new_reservation
        ├── availability_request
        ├── confirm_proposal
        ├── modify_reservation
        ├── cancel_reservation
        ├── faq
        ├── human_handoff
        └── unknown

Ajoutez un contrôle indépendant des mots sensibles

Ne dépendez pas uniquement de la classification IA pour détecter une allergie ou une plainte. Avant de poursuivre, ajoutez un nœud Code qui analyse le texte normalisé et force un transfert humain lorsqu’un terme sensible est détecté.

Voici un exemple de logique indicative :

const text = ($json.message_text || '').toLowerCase();

const sensitiveTerms = [
  'allergie',
  'anaphylaxie',
  'choc allergique',
  'contamination croisée',
  'médical',
  'urgence',
  'privatisation',
  'devis',
  'groupe',
  'réclamation',
  'plainte',
  'facture',
  'litige',
  'remboursement'
];

const detectedTerms = sensitiveTerms.filter((term) => text.includes(term));

return [
  {
    json: {
      ...$json,
      detected_sensitive_terms: detectedTerms,
      force_human_review: detectedTerms.length > 0
    }
  }
];

Cette liste ne doit pas être considérée comme exhaustive. Elle sert de seconde barrière simple. Vous pourrez l’adapter à la langue du restaurant, aux demandes réellement reçues et aux retours de l’équipe.

Créez les fonctions Supabase qui protègent les actions critiques

Dans la partie 2, vous avez créé une fonction de recherche de tables disponibles et une contrainte qui empêche les chevauchements sur une même table. Vous allez maintenant créer une fonction pour enregistrer une réservation temporaire.

Cette fonction doit faire plusieurs choses dans une seule opération : vérifier les paramètres, créer la réservation, affecter la table et écrire un événement. Si l’affectation de table entre en conflit avec une autre réservation, l’ensemble de l’opération doit échouer. n8n pourra alors proposer un autre créneau ou transférer la demande à l’équipe.

create or replace function public.create_reservation_hold(
  p_restaurant_id uuid,
  p_customer_id uuid,
  p_table_id uuid,
  p_party_size integer,
  p_starts_at timestamptz,
  p_ends_at timestamptz,
  p_customer_name text,
  p_customer_phone text,
  p_language_code text,
  p_seating_preference text,
  p_special_request text,
  p_allergy_notice text,
  p_needs_human_review boolean,
  p_hold_minutes integer default 10
)
returns uuid
language plpgsql
security definer
set search_path = public
as $$
declare
  v_reservation_id uuid;
  v_hold_expires_at timestamptz;
begin
  if p_party_size < 1 or p_party_size > 50 then
    raise exception 'Invalid party size';
  end if;

  if p_ends_at <= p_starts_at then
    raise exception 'Invalid reservation time range';
  end if;

  if p_hold_minutes < 1 or p_hold_minutes > 30 then
    raise exception 'Invalid hold duration';
  end if;

  v_hold_expires_at := now() + make_interval(mins => p_hold_minutes);

  insert into public.reservations (
    restaurant_id,
    customer_id,
    status,
    source,
    party_size,
    starts_at,
    ends_at,
    customer_name,
    customer_phone,
    language_code,
    seating_preference,
    special_request,
    allergy_notice,
    needs_human_review,
    hold_expires_at,
    created_by,
    updated_by
  )
  values (
    p_restaurant_id,
    p_customer_id,
    'hold',
    'whatsapp',
    p_party_size,
    p_starts_at,
    p_ends_at,
    p_customer_name,
    p_customer_phone,
    p_language_code,
    p_seating_preference,
    p_special_request,
    p_allergy_notice,
    p_needs_human_review,
    v_hold_expires_at,
    'n8n:create_reservation_hold',
    'n8n:create_reservation_hold'
  )
  returning id into v_reservation_id;

  insert into public.reservation_table_assignments (
    reservation_id,
    restaurant_table_id,
    starts_at,
    ends_at
  )
  values (
    v_reservation_id,
    p_table_id,
    p_starts_at,
    p_ends_at
  );

  insert into public.reservation_events (
    reservation_id,
    event_type,
    actor_type,
    actor_reference,
    payload
  )
  values (
    v_reservation_id,
    'hold_created',
    'workflow',
    'n8n',
    jsonb_build_object(
      'hold_expires_at', v_hold_expires_at,
      'table_id', p_table_id
    )
  );

  return v_reservation_id;
end;
$$;

La contrainte d’exclusion créée dans la partie 2 protège cette fonction. Si une autre réservation occupe déjà la table sur la même période, l’insertion dans reservation_table_assignments échoue. Comme les opérations sont exécutées dans la même fonction, la réservation temporaire ne doit pas rester créée sans table affectée.

Créez ensuite une fonction de confirmation. Elle doit vérifier que la réservation est encore en attente, que son blocage n’a pas expiré et qu’aucune validation humaine n’est requise.

create or replace function public.confirm_reservation_hold(
  p_reservation_id uuid,
  p_confirmation_code text
)
returns public.reservations
language plpgsql
security definer
set search_path = public
as $$
declare
  v_reservation public.reservations;
begin
  select *
  into v_reservation
  from public.reservations
  where id = p_reservation_id
  for update;

  if not found then
    raise exception 'Reservation not found';
  end if;

  if v_reservation.status <> 'hold' then
    raise exception 'Reservation is not awaiting confirmation';
  end if;

  if v_reservation.hold_expires_at is null
    or v_reservation.hold_expires_at <= now() then
    update public.reservations
    set
      status = 'expired',
      updated_at = now(),
      updated_by = 'n8n:confirm_reservation_hold'
    where id = p_reservation_id;

    raise exception 'Reservation hold has expired';
  end if;

  if v_reservation.needs_human_review = true then
    raise exception 'Reservation requires human review';
  end if;

  update public.reservations
  set
    status = 'confirmed',
    confirmation_code = p_confirmation_code,
    hold_expires_at = null,
    updated_at = now(),
    updated_by = 'n8n:confirm_reservation_hold'
  where id = p_reservation_id
  returning * into v_reservation;

  insert into public.reservation_events (
    reservation_id,
    event_type,
    actor_type,
    actor_reference,
    payload
  )
  values (
    p_reservation_id,
    'confirmed',
    'workflow',
    'n8n',
    jsonb_build_object(
      'confirmation_code', p_confirmation_code
    )
  );

  return v_reservation;
end;
$$;

Le code de confirmation doit être généré par n8n ou par une fonction dédiée avec une longueur et une unicité adaptées. Il peut être utile pour permettre au client de retrouver sa réservation, mais il ne doit jamais être le seul mécanisme de sécurité pour une modification sensible. Le numéro WhatsApp du client, l’identifiant de conversation et des contrôles supplémentaires doivent également être pris en compte.

Construisez le workflow d’une nouvelle réservation

Une nouvelle réservation ne doit pas être créée en une seule étape. Le workflow doit d’abord s’assurer que les informations nécessaires sont présentes, puis rechercher les tables, créer éventuellement un blocage temporaire et demander une confirmation explicite au client.

Voici le parcours de référence :

  1. Le message est classé avec l’intention new_reservation ou availability_request.
  2. n8n vérifie les champs requis : date, heure, nombre de personnes et nom si votre règle l’exige avant la confirmation.
  3. Si une donnée manque, n8n envoie la question proposée ou une formulation validée, puis met à jour current_step dans la conversation.
  4. Si une allergie, une demande médicale ou un autre signal sensible est détecté, n8n crée une demande humaine au lieu de poursuivre.
  5. Si les données sont complètes, n8n calcule l’heure de fin selon les règles de service et appelle find_available_tables.
  6. Si aucune table n’est disponible, n8n recherche des alternatives ou demande à l’équipe de vérifier une solution.
  7. Si une table est disponible, n8n appelle create_reservation_hold.
  8. n8n envoie une proposition claire au client avec le créneau, le nombre de couverts et une demande de confirmation.
  9. Lorsque le client confirme, l’IA classe le message en confirm_proposal et n8n appelle confirm_reservation_hold.
  10. n8n envoie un récapitulatif et met à jour la conversation.

Le message de proposition doit indiquer qu’il s’agit d’une réservation temporairement bloquée et qu’une réponse explicite est attendue. Évitez les formulations ambiguës telles que « votre table est réservée » si vous attendez encore une confirmation.

Bonjour, nous pouvons vous proposer une table pour 4 personnes le vendredi 18 septembre à 20 h 00.

Pour confirmer cette proposition, répondez simplement CONFIRMER dans les prochaines minutes. Si vous préférez un autre horaire, indiquez-nous l’heure souhaitée.

La durée exacte de blocage ne doit pas être annoncée si vous ne souhaitez pas créer de pression inutile. Elle doit toutefois être définie dans le workflow et appliquée de manière cohérente par Supabase.

Comment gérer un créneau indisponible

Si aucune table ne correspond à la demande, l’assistant ne doit pas répondre « complet » sans vérifier s’il existe des alternatives utiles. Vous pouvez rechercher, par exemple, le créneau précédent et le créneau suivant selon l’intervalle choisi par le restaurant.

Cette recherche doit rester déterministe. n8n peut appeler plusieurs fois la fonction de disponibilité avec des horaires précis, par exemple trente minutes avant et trente minutes après la demande. Il ne faut pas laisser le modèle inventer « 19 h 30 » comme alternative sans l’avoir vérifié dans la base.

Si aucune alternative ne convient ou si la demande concerne un groupe, transférez le cas à l’équipe. Le client doit alors recevoir une réponse transparente, par exemple :

Nous vérifions votre demande auprès de notre équipe afin de vous proposer la meilleure solution possible. Nous revenons vers vous dès que possible.

Gérez les modifications sans créer de réservation fantôme

Une modification est plus complexe qu’une nouvelle réservation, car elle peut concerner une réservation déjà confirmée, une réservation temporaire ou plusieurs réservations proches dans le temps. Ne demandez pas à l’IA de modifier directement une ligne Supabase à partir d’un message libre.

Le workflow doit d’abord identifier la réservation concernée. Utilisez, selon votre modèle, le numéro WhatsApp, le restaurant, la date de réservation, le code de confirmation ou une combinaison de ces éléments. Si plusieurs réservations sont possibles, posez une question de clarification ou transférez le cas à une personne.

Le parcours recommandé est le suivant :

  1. Identifier la réservation active associée au client.
  2. Vérifier que la demande de modification est suffisamment précise.
  3. Si nécessaire, demander la nouvelle date, heure ou taille de groupe.
  4. Rechercher une disponibilité pour le nouveau créneau avant de libérer l’ancien.
  5. Créer un blocage temporaire sur la nouvelle table ou le nouveau créneau.
  6. Demander au client de confirmer la modification proposée.
  7. Après confirmation, mettre à jour la réservation et l’affectation de table dans une opération contrôlée.
  8. Journaliser l’ancienne valeur, la nouvelle valeur et le canal de demande.

Ne libérez pas l’ancienne table avant que la nouvelle solution soit validée. Sinon, le client peut perdre sa réservation initiale alors que le créneau souhaité n’est finalement pas disponible.

Pour les premières versions, une approche prudente consiste à transférer toutes les modifications à l’équipe, puis à automatiser progressivement les cas simples une fois que vous avez observé les demandes réelles et validé les règles.

Gérez les annulations avec une trace claire

Une annulation paraît simple, mais elle peut avoir des conséquences sur la capacité, les rappels, les acomptes, les listes d’attente et les indicateurs de no-show. Ne supprimez pas une réservation confirmée de la base : modifiez son statut en cancelled et enregistrez un événement.

Créez une fonction de base pour annuler une réservation. Cette fonction doit identifier la réservation, empêcher l’annulation d’un dossier déjà terminé ou déjà annulé et consigner la demande.

create or replace function public.cancel_reservation(
  p_reservation_id uuid,
  p_actor_type text,
  p_actor_reference text,
  p_reason text default null
)
returns public.reservations
language plpgsql
security definer
set search_path = public
as $$
declare
  v_reservation public.reservations;
begin
  select *
  into v_reservation
  from public.reservations
  where id = p_reservation_id
  for update;

  if not found then
    raise exception 'Reservation not found';
  end if;

  if v_reservation.status in ('cancelled', 'completed', 'no_show') then
    raise exception 'Reservation cannot be cancelled in its current status';
  end if;

  update public.reservations
  set
    status = 'cancelled',
    hold_expires_at = null,
    updated_at = now(),
    updated_by = p_actor_type || ':' || coalesce(p_actor_reference, 'unknown')
  where id = p_reservation_id
  returning * into v_reservation;

  insert into public.reservation_events (
    reservation_id,
    event_type,
    actor_type,
    actor_reference,
    payload
  )
  values (
    p_reservation_id,
    'cancelled',
    p_actor_type,
    p_actor_reference,
    jsonb_build_object('reason', p_reason)
  );

  return v_reservation;
end;
$$;

Dans votre logique de disponibilité, une réservation annulée ne doit plus bloquer une table. Vous pouvez conserver les affectations de table à des fins d’historique, car votre fonction de disponibilité exclut déjà les statuts autres que hold et confirmed.

Le message envoyé au client doit confirmer l’annulation avec des informations simples, sans divulguer de données inutiles :

Votre réservation du vendredi 18 septembre à 20 h 00 a bien été annulée. Si vous souhaitez réserver un autre créneau, indiquez-nous la date, l’heure et le nombre de personnes.

Créez une file d’escalade humaine visible par l’équipe

Un bon assistant n’essaie pas de tout résoudre. Il doit fournir à l’équipe les informations nécessaires pour reprendre rapidement la conversation, sans demander au client de tout répéter.

Ajoutez une table de file humaine dans Supabase :

create type public.handoff_status as enum (
  'open',
  'assigned',
  'resolved',
  'closed'
);

create table public.handoff_queue (
  id uuid primary key default gen_random_uuid(),
  restaurant_id uuid not null references public.restaurants(id) on delete cascade,
  customer_id uuid references public.customers(id) on delete set null,
  conversation_id uuid references public.conversations(id) on delete set null,
  reservation_id uuid references public.reservations(id) on delete set null,
  status public.handoff_status not null default 'open',
  priority text not null default 'normal',
  reason text not null,
  summary text,
  assigned_to text,
  resolved_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index handoff_queue_restaurant_status_idx
  on public.handoff_queue (restaurant_id, status, created_at asc);

create trigger set_handoff_queue_updated_at
before update on public.handoff_queue
for each row
execute function public.set_updated_at();

Lorsqu’un transfert est nécessaire, n8n doit créer une ligne dans cette table, puis envoyer une notification à l’équipe selon votre outil interne. La notification doit contenir le minimum utile : nom si connu, téléphone, résumé de la demande, créneau éventuel, motif de transfert et lien vers l’interface interne lorsque celle-ci existe réellement.

Le client doit recevoir une réponse honnête. Ne dites pas qu’un membre de l’équipe « répondra dans deux minutes » si vous ne pouvez pas tenir ce délai. Préférez une formulation adaptée à votre organisation :

Merci pour votre demande. Elle nécessite une vérification par notre équipe. Nous vous répondrons dès que possible.

Testez les scénarios difficiles avant toute mise en production

Un workflow de réservation doit être testé avec des messages incomplets, ambigus et contradictoires, pas seulement avec une demande parfaite. Conservez les résultats de test afin d’améliorer le prompt, les règles n8n et les procédures de reprise humaine.

  • « Une table ce soir ? » : vérifier que l’assistant demande le nombre de personnes et l’heure.
  • « Demain à 20 h » : vérifier que l’assistant demande le nombre de couverts et interprète la date à partir du fuseau horaire du restaurant.
  • « On est six, dehors si possible » : vérifier que la terrasse est traitée comme préférence ou contrainte selon vos règles.
  • « Je confirme » : vérifier que l’assistant ne confirme qu’une proposition active associée à la conversation.
  • « Finalement, nous serons huit » : vérifier que le cas est transféré si huit dépasse votre limite automatique.
  • « Mon enfant est allergique aux arachides » : vérifier que le système crée une demande humaine sans promettre une prise en charge.
  • « Annulez ma réservation » : vérifier que le système identifie une réservation unique avant d’annuler.
  • « Je veux changer vendredi pour samedi » : vérifier que la nouvelle disponibilité est trouvée avant de modifier l’ancienne réservation.
  • « Vous avez oublié ma réservation » : vérifier que le message est classé comme plainte ou litige et envoyé à l’équipe.
  • Message vide, audio, image ou document : vérifier que le système enregistre l’événement et demande une action adaptée au lieu d’échouer silencieusement.
  • Deux demandes simultanées sur la même table : vérifier que la contrainte PostgreSQL empêche la double affectation.
  • Réponse IA invalide ou interruption du fournisseur : vérifier que n8n ne poursuit pas vers une action de réservation et crée une alerte interne.

Testez également les cas de langage naturel : fautes de frappe, formulations familières, messages en plusieurs parties, horaires approximatifs et changements d’avis. L’objectif n’est pas d’obtenir une compréhension parfaite de chaque message, mais de garantir que le système se comporte de manière prudente lorsqu’il ne comprend pas.

Conclusion de la partie 4

Vous disposez maintenant du principe central de l’assistant : l’intelligence artificielle transforme une demande libre en intention et en données structurées, sans décider seule de la disponibilité ni de la confirmation. Supabase protège les réservations et n8n orchestre les branches de workflow, les questions complémentaires, les messages de proposition et les transferts humains.

Dans la partie 5, vous allez rendre le système exploitable au quotidien. Vous créerez les rappels, les confirmations avant service, le suivi des annulations et no-shows, les indicateurs utiles, les scénarios de reprise après erreur et les extensions possibles vers les avis clients, le Click & Collect ou la fidélisation.

Explorer les analyses sur les chatbots et agents IA

Sources et pour aller plus loin

Partie 4 : créer un agent IA de réservation restaurant sans lui laisser décider seul

Votre système peut maintenant recevoir un message WhatsApp, identifier un client, enregistrer les messages et conserver un état de conversation dans Supabase. Vous allez ajouter l’intelligence artificielle pour comprendre ce que le client demande, extraire les informations utiles et orienter le workflow.

Cette étape ne consiste pas à laisser un modèle de langage gérer seul les réservations. Un modèle peut interpréter un message écrit de façon naturelle, mais il peut aussi mal comprendre une date, ignorer une nuance ou produire une réponse convaincante alors que la disponibilité n’a pas été vérifiée. Dans ce tutoriel, l’IA prépare la décision. Supabase et les règles métier prennent la décision. L’équipe du restaurant traite les exceptions.

À retenir : utilisez l’IA pour transformer un message libre en données structurées, pas pour inventer une réponse opérationnelle. Une disponibilité, une allergie, un changement de table ou une annulation doivent toujours être vérifiés par les règles métier et la base de données avant qu’un message définitif soit envoyé au client.

Pourquoi l’agent IA ne doit jamais confirmer une réservation seul

Un client peut formuler une demande de multiples façons : « Une table demain soir ? », « Vous avez encore de la place vers 21 heures pour trois ? », « Je voudrais modifier notre réservation de vendredi » ou « Mon fils est allergique aux arachides, est-ce que vous pouvez l’accueillir ? ».

Le rôle de l’IA est utile à ce stade, car elle peut reconnaître l’intention et extraire une information à partir d’un texte libre. Elle peut identifier une date, un horaire approximatif, un nombre de personnes, une préférence de terrasse ou une demande de modification.

Elle ne doit toutefois jamais produire une confirmation définitive à partir de cette seule compréhension. Elle ne connaît pas automatiquement :

  • Les tables déjà réservées et les réservations temporaires actives.
  • Les fermetures exceptionnelles et les réductions de capacité.
  • Les règles particulières selon le jour, le service ou la taille du groupe.
  • Les changements effectués manuellement par l’équipe entre deux messages.
  • Les capacités réelles de la cuisine à gérer une allergie ou une demande spécifique.
  • Les restrictions applicables aux annulations, aux acomptes ou aux événements.

Une architecture sûre doit donc respecter la séquence suivante :

Message WhatsApp du client
        ↓
IA : intention et données structurées
        ↓
n8n : validation des champs et règles de routage
        ↓
Supabase : disponibilité, contraintes et opérations atomiques
        ↓
n8n : construction d’une réponse fondée sur le résultat réel
        ↓
WhatsApp : proposition, confirmation ou transfert humain

Définissez un contrat de sortie JSON pour l’agent

La sortie de l’agent doit être exploitable par n8n sans qu’un workflow tente d’interpréter des phrases libres. Au lieu de demander au modèle de répondre « Je pense que le client veut une réservation demain », demandez-lui de retourner des champs déterminés.

Les technologies de sortie structurée permettent d’imposer un schéma JSON. Cette méthode réduit les erreurs de format et empêche par exemple le modèle d’inventer une intention non prévue par votre workflow. Elle ne dispense pas de vérifier les valeurs extraites : une date peut être syntaxiquement valide tout en étant mal interprétée.

Pour ce tutoriel, utilisez les intentions suivantes :

Intention Quand l’utiliser Action autorisée ensuite
new_reservation Le client souhaite réserver une nouvelle table Collecter les champs, puis vérifier la disponibilité
availability_request Le client demande simplement si un créneau est disponible Demander les informations manquantes ou rechercher les créneaux
confirm_proposal Le client confirme un créneau proposé par le système Vérifier une dernière fois et confirmer si les règles le permettent
modify_reservation Le client souhaite changer un horaire, une date ou un nombre de couverts Identifier la réservation, libérer ou retenir le nouveau créneau selon le workflow
cancel_reservation Le client souhaite annuler Identifier la réservation et appliquer la politique définie
faq Le client pose une question sur les horaires, l’adresse ou les conditions générales Répondre uniquement à partir d’une base de réponses validées
human_handoff Le message exige une vérification humaine Créer une demande dans la file d’intervention
unknown L’intention ne peut pas être comprise avec assez de certitude Demander une précision ou transférer selon le contexte

Ajoutez également un niveau de risque. Il sert à empêcher qu’un message contenant une allergie grave, une plainte ou une demande de privatisation poursuive automatiquement vers la réservation, même si le modèle a également identifié une date et un nombre de couverts.

{
  "intent": "new_reservation",
  "confidence": "high",
  "risk_level": "normal",
  "requires_human_review": false,
  "reason_for_handoff": null,
  "party_size": 4,
  "requested_date": "2026-09-18",
  "requested_time": "20:00",
  "time_is_flexible": false,
  "customer_name": null,
  "seating_preference": "terrace_preferred",
  "accessibility_needed": false,
  "allergy_or_medical_request": false,
  "special_request": null,
  "reservation_reference": null,
  "missing_fields": ["customer_name"],
  "suggested_customer_question": "Pour finaliser votre demande, quel nom devons-nous indiquer pour la réservation ?"
}

Dans ce schéma, le modèle n’a pas le droit de renvoyer un champ du type reservation_confirmed, available_table ou allergy_supported. Ces décisions appartiennent au workflow et à la base de données.

Écrivez un prompt système limité et explicite

Le prompt système ne doit pas demander à l’IA d’être « un excellent assistant de restaurant » sans cadre précis. Il doit définir ce qu’elle peut faire, ce qu’elle ne peut pas faire, les intentions autorisées, les règles de prudence et le format de sortie attendu.

Dans n8n, vous pouvez utiliser un nœud AI Agent avec un analyseur de sortie structurée, ou appeler directement un fournisseur de modèle qui prend en charge les sorties structurées. Le choix dépend de votre version n8n, de votre fournisseur, de vos contraintes de données et de votre architecture. Quelle que soit l’option choisie, conservez le même contrat JSON et validez sa sortie avant toute action.

Voici un prompt système de départ, à adapter avec vos propres règles restaurant :

Vous êtes un analyseur de messages pour un assistant de réservation de restaurant.

Votre mission est uniquement de :
1. classifier l’intention du client ;
2. extraire les informations explicitement présentes dans le message ;
3. détecter les demandes nécessitant une intervention humaine ;
4. retourner exclusivement un objet conforme au schéma JSON fourni.

Vous ne confirmez jamais une réservation.
Vous ne déclarez jamais qu’un créneau est disponible.
Vous ne promettez jamais une table, une terrasse, une préférence de placement, un menu ou une prestation.
Vous ne confirmez jamais la prise en charge d’une allergie, d’un régime médical ou d’une demande de santé.
Vous n’inventez jamais de date, d’heure, de nombre de personnes, de coordonnées, de politique ou de référence de réservation.
Si une information est ambiguë, absente ou contradictoire, ajoutez-la à missing_fields ou choisissez l’intention unknown.
Si le message mentionne une allergie grave, anaphylaxie, urgence, groupe important, privatisation, devis, plainte, litige, facture, paiement, harcèlement ou menace, définissez requires_human_review à true et choisissez human_handoff lorsque cela est nécessaire.
Utilisez uniquement les intentions prévues par le schéma.
Retournez uniquement le JSON structuré, sans phrase autour.

Le prompt doit recevoir le contexte nécessaire, mais pas davantage. Transmettez par exemple le dernier message, l’étape de conversation actuelle, la langue connue du client et les éléments déjà confirmés. Évitez d’envoyer une longue archive de messages ou des données personnelles qui ne sont pas utiles à l’interprétation.

Construisez le workflow n8n de classification et d’extraction

Créez un workflow nommé WA-03 — Classification et extraction IA. Il reçoit les données normalisées du workflow de réception construit dans la partie 3, puis retourne une sortie structurée que n8n peut utiliser.

Le workflow doit suivre une progression simple :

  1. Recevoir le message normalisé, le client et la conversation active.
  2. Vérifier que le message est de type texte et qu’il contient un contenu exploitable.
  3. Récupérer le contexte de conversation limité depuis Supabase.
  4. Envoyer le message et le contexte au modèle avec le prompt système.
  5. Valider que la sortie respecte le schéma attendu.
  6. Vérifier les règles de sécurité indépendamment de l’IA, par exemple avec une liste de mots-clés sensibles.
  7. Router le résultat vers une branche de réservation, de modification, d’annulation, de FAQ ou de transfert humain.

Dans n8n, activez une sortie structurée lorsque votre modèle et votre nœud le permettent. Le nœud Tools Agent de n8n peut imposer un format de sortie et utiliser notamment un Structured Output Parser. Vous pouvez également demander une validation humaine avant l’exécution d’outils sensibles.

Ajoutez un nœud Switch après la sortie structurée. Il doit orienter le workflow selon la valeur intent. Ne créez pas un seul long workflow avec des conditions dispersées. Des branches séparées rendent les erreurs plus visibles et facilitent les tests.

Sortie IA structurée
        ↓
Contrôle de sécurité n8n
        ↓
Switch sur intent
        ├── new_reservation
        ├── availability_request
        ├── confirm_proposal
        ├── modify_reservation
        ├── cancel_reservation
        ├── faq
        ├── human_handoff
        └── unknown

Ajoutez un contrôle indépendant des mots sensibles

Ne dépendez pas uniquement de la classification IA pour détecter une allergie ou une plainte. Avant de poursuivre, ajoutez un nœud Code qui analyse le texte normalisé et force un transfert humain lorsqu’un terme sensible est détecté.

Voici un exemple de logique indicative :

const text = ($json.message_text || '').toLowerCase();

const sensitiveTerms = [
  'allergie',
  'anaphylaxie',
  'choc allergique',
  'contamination croisée',
  'médical',
  'urgence',
  'privatisation',
  'devis',
  'groupe',
  'réclamation',
  'plainte',
  'facture',
  'litige',
  'remboursement'
];

const detectedTerms = sensitiveTerms.filter((term) => text.includes(term));

return [
  {
    json: {
      ...$json,
      detected_sensitive_terms: detectedTerms,
      force_human_review: detectedTerms.length > 0
    }
  }
];

Cette liste ne doit pas être considérée comme exhaustive. Elle sert de seconde barrière simple. Vous pourrez l’adapter à la langue du restaurant, aux demandes réellement reçues et aux retours de l’équipe.

Créez les fonctions Supabase qui protègent les actions critiques

Dans la partie 2, vous avez créé une fonction de recherche de tables disponibles et une contrainte qui empêche les chevauchements sur une même table. Vous allez maintenant créer une fonction pour enregistrer une réservation temporaire.

Cette fonction doit faire plusieurs choses dans une seule opération : vérifier les paramètres, créer la réservation, affecter la table et écrire un événement. Si l’affectation de table entre en conflit avec une autre réservation, l’ensemble de l’opération doit échouer. n8n pourra alors proposer un autre créneau ou transférer la demande à l’équipe.

create or replace function public.create_reservation_hold(
  p_restaurant_id uuid,
  p_customer_id uuid,
  p_table_id uuid,
  p_party_size integer,
  p_starts_at timestamptz,
  p_ends_at timestamptz,
  p_customer_name text,
  p_customer_phone text,
  p_language_code text,
  p_seating_preference text,
  p_special_request text,
  p_allergy_notice text,
  p_needs_human_review boolean,
  p_hold_minutes integer default 10
)
returns uuid
language plpgsql
security definer
set search_path = public
as $$
declare
  v_reservation_id uuid;
  v_hold_expires_at timestamptz;
begin
  if p_party_size < 1 or p_party_size > 50 then
    raise exception 'Invalid party size';
  end if;

  if p_ends_at <= p_starts_at then
    raise exception 'Invalid reservation time range';
  end if;

  if p_hold_minutes < 1 or p_hold_minutes > 30 then
    raise exception 'Invalid hold duration';
  end if;

  v_hold_expires_at := now() + make_interval(mins => p_hold_minutes);

  insert into public.reservations (
    restaurant_id,
    customer_id,
    status,
    source,
    party_size,
    starts_at,
    ends_at,
    customer_name,
    customer_phone,
    language_code,
    seating_preference,
    special_request,
    allergy_notice,
    needs_human_review,
    hold_expires_at,
    created_by,
    updated_by
  )
  values (
    p_restaurant_id,
    p_customer_id,
    'hold',
    'whatsapp',
    p_party_size,
    p_starts_at,
    p_ends_at,
    p_customer_name,
    p_customer_phone,
    p_language_code,
    p_seating_preference,
    p_special_request,
    p_allergy_notice,
    p_needs_human_review,
    v_hold_expires_at,
    'n8n:create_reservation_hold',
    'n8n:create_reservation_hold'
  )
  returning id into v_reservation_id;

  insert into public.reservation_table_assignments (
    reservation_id,
    restaurant_table_id,
    starts_at,
    ends_at
  )
  values (
    v_reservation_id,
    p_table_id,
    p_starts_at,
    p_ends_at
  );

  insert into public.reservation_events (
    reservation_id,
    event_type,
    actor_type,
    actor_reference,
    payload
  )
  values (
    v_reservation_id,
    'hold_created',
    'workflow',
    'n8n',
    jsonb_build_object(
      'hold_expires_at', v_hold_expires_at,
      'table_id', p_table_id
    )
  );

  return v_reservation_id;
end;
$$;

La contrainte d’exclusion créée dans la partie 2 protège cette fonction. Si une autre réservation occupe déjà la table sur la même période, l’insertion dans reservation_table_assignments échoue. Comme les opérations sont exécutées dans la même fonction, la réservation temporaire ne doit pas rester créée sans table affectée.

Créez ensuite une fonction de confirmation. Elle doit vérifier que la réservation est encore en attente, que son blocage n’a pas expiré et qu’aucune validation humaine n’est requise.

create or replace function public.confirm_reservation_hold(
  p_reservation_id uuid,
  p_confirmation_code text
)
returns public.reservations
language plpgsql
security definer
set search_path = public
as $$
declare
  v_reservation public.reservations;
begin
  select *
  into v_reservation
  from public.reservations
  where id = p_reservation_id
  for update;

  if not found then
    raise exception 'Reservation not found';
  end if;

  if v_reservation.status <> 'hold' then
    raise exception 'Reservation is not awaiting confirmation';
  end if;

  if v_reservation.hold_expires_at is null
    or v_reservation.hold_expires_at <= now() then
    update public.reservations
    set
      status = 'expired',
      updated_at = now(),
      updated_by = 'n8n:confirm_reservation_hold'
    where id = p_reservation_id;

    raise exception 'Reservation hold has expired';
  end if;

  if v_reservation.needs_human_review = true then
    raise exception 'Reservation requires human review';
  end if;

  update public.reservations
  set
    status = 'confirmed',
    confirmation_code = p_confirmation_code,
    hold_expires_at = null,
    updated_at = now(),
    updated_by = 'n8n:confirm_reservation_hold'
  where id = p_reservation_id
  returning * into v_reservation;

  insert into public.reservation_events (
    reservation_id,
    event_type,
    actor_type,
    actor_reference,
    payload
  )
  values (
    p_reservation_id,
    'confirmed',
    'workflow',
    'n8n',
    jsonb_build_object(
      'confirmation_code', p_confirmation_code
    )
  );

  return v_reservation;
end;
$$;

Le code de confirmation doit être généré par n8n ou par une fonction dédiée avec une longueur et une unicité adaptées. Il peut être utile pour permettre au client de retrouver sa réservation, mais il ne doit jamais être le seul mécanisme de sécurité pour une modification sensible. Le numéro WhatsApp du client, l’identifiant de conversation et des contrôles supplémentaires doivent également être pris en compte.

Construisez le workflow d’une nouvelle réservation

Une nouvelle réservation ne doit pas être créée en une seule étape. Le workflow doit d’abord s’assurer que les informations nécessaires sont présentes, puis rechercher les tables, créer éventuellement un blocage temporaire et demander une confirmation explicite au client.

Voici le parcours de référence :

  1. Le message est classé avec l’intention new_reservation ou availability_request.
  2. n8n vérifie les champs requis : date, heure, nombre de personnes et nom si votre règle l’exige avant la confirmation.
  3. Si une donnée manque, n8n envoie la question proposée ou une formulation validée, puis met à jour current_step dans la conversation.
  4. Si une allergie, une demande médicale ou un autre signal sensible est détecté, n8n crée une demande humaine au lieu de poursuivre.
  5. Si les données sont complètes, n8n calcule l’heure de fin selon les règles de service et appelle find_available_tables.
  6. Si aucune table n’est disponible, n8n recherche des alternatives ou demande à l’équipe de vérifier une solution.
  7. Si une table est disponible, n8n appelle create_reservation_hold.
  8. n8n envoie une proposition claire au client avec le créneau, le nombre de couverts et une demande de confirmation.
  9. Lorsque le client confirme, l’IA classe le message en confirm_proposal et n8n appelle confirm_reservation_hold.
  10. n8n envoie un récapitulatif et met à jour la conversation.

Le message de proposition doit indiquer qu’il s’agit d’une réservation temporairement bloquée et qu’une réponse explicite est attendue. Évitez les formulations ambiguës telles que « votre table est réservée » si vous attendez encore une confirmation.

Bonjour, nous pouvons vous proposer une table pour 4 personnes le vendredi 18 septembre à 20 h 00.

Pour confirmer cette proposition, répondez simplement CONFIRMER dans les prochaines minutes. Si vous préférez un autre horaire, indiquez-nous l’heure souhaitée.

La durée exacte de blocage ne doit pas être annoncée si vous ne souhaitez pas créer de pression inutile. Elle doit toutefois être définie dans le workflow et appliquée de manière cohérente par Supabase.

Comment gérer un créneau indisponible

Si aucune table ne correspond à la demande, l’assistant ne doit pas répondre « complet » sans vérifier s’il existe des alternatives utiles. Vous pouvez rechercher, par exemple, le créneau précédent et le créneau suivant selon l’intervalle choisi par le restaurant.

Cette recherche doit rester déterministe. n8n peut appeler plusieurs fois la fonction de disponibilité avec des horaires précis, par exemple trente minutes avant et trente minutes après la demande. Il ne faut pas laisser le modèle inventer « 19 h 30 » comme alternative sans l’avoir vérifié dans la base.

Si aucune alternative ne convient ou si la demande concerne un groupe, transférez le cas à l’équipe. Le client doit alors recevoir une réponse transparente, par exemple :

Nous vérifions votre demande auprès de notre équipe afin de vous proposer la meilleure solution possible. Nous revenons vers vous dès que possible.

Gérez les modifications sans créer de réservation fantôme

Une modification est plus complexe qu’une nouvelle réservation, car elle peut concerner une réservation déjà confirmée, une réservation temporaire ou plusieurs réservations proches dans le temps. Ne demandez pas à l’IA de modifier directement une ligne Supabase à partir d’un message libre.

Le workflow doit d’abord identifier la réservation concernée. Utilisez, selon votre modèle, le numéro WhatsApp, le restaurant, la date de réservation, le code de confirmation ou une combinaison de ces éléments. Si plusieurs réservations sont possibles, posez une question de clarification ou transférez le cas à une personne.

Le parcours recommandé est le suivant :

  1. Identifier la réservation active associée au client.
  2. Vérifier que la demande de modification est suffisamment précise.
  3. Si nécessaire, demander la nouvelle date, heure ou taille de groupe.
  4. Rechercher une disponibilité pour le nouveau créneau avant de libérer l’ancien.
  5. Créer un blocage temporaire sur la nouvelle table ou le nouveau créneau.
  6. Demander au client de confirmer la modification proposée.
  7. Après confirmation, mettre à jour la réservation et l’affectation de table dans une opération contrôlée.
  8. Journaliser l’ancienne valeur, la nouvelle valeur et le canal de demande.

Ne libérez pas l’ancienne table avant que la nouvelle solution soit validée. Sinon, le client peut perdre sa réservation initiale alors que le créneau souhaité n’est finalement pas disponible.

Pour les premières versions, une approche prudente consiste à transférer toutes les modifications à l’équipe, puis à automatiser progressivement les cas simples une fois que vous avez observé les demandes réelles et validé les règles.

Gérez les annulations avec une trace claire

Une annulation paraît simple, mais elle peut avoir des conséquences sur la capacité, les rappels, les acomptes, les listes d’attente et les indicateurs de no-show. Ne supprimez pas une réservation confirmée de la base : modifiez son statut en cancelled et enregistrez un événement.

Créez une fonction de base pour annuler une réservation. Cette fonction doit identifier la réservation, empêcher l’annulation d’un dossier déjà terminé ou déjà annulé et consigner la demande.

create or replace function public.cancel_reservation(
  p_reservation_id uuid,
  p_actor_type text,
  p_actor_reference text,
  p_reason text default null
)
returns public.reservations
language plpgsql
security definer
set search_path = public
as $$
declare
  v_reservation public.reservations;
begin
  select *
  into v_reservation
  from public.reservations
  where id = p_reservation_id
  for update;

  if not found then
    raise exception 'Reservation not found';
  end if;

  if v_reservation.status in ('cancelled', 'completed', 'no_show') then
    raise exception 'Reservation cannot be cancelled in its current status';
  end if;

  update public.reservations
  set
    status = 'cancelled',
    hold_expires_at = null,
    updated_at = now(),
    updated_by = p_actor_type || ':' || coalesce(p_actor_reference, 'unknown')
  where id = p_reservation_id
  returning * into v_reservation;

  insert into public.reservation_events (
    reservation_id,
    event_type,
    actor_type,
    actor_reference,
    payload
  )
  values (
    p_reservation_id,
    'cancelled',
    p_actor_type,
    p_actor_reference,
    jsonb_build_object('reason', p_reason)
  );

  return v_reservation;
end;
$$;

Dans votre logique de disponibilité, une réservation annulée ne doit plus bloquer une table. Vous pouvez conserver les affectations de table à des fins d’historique, car votre fonction de disponibilité exclut déjà les statuts autres que hold et confirmed.

Le message envoyé au client doit confirmer l’annulation avec des informations simples, sans divulguer de données inutiles :

Votre réservation du vendredi 18 septembre à 20 h 00 a bien été annulée. Si vous souhaitez réserver un autre créneau, indiquez-nous la date, l’heure et le nombre de personnes.

Créez une file d’escalade humaine visible par l’équipe

Un bon assistant n’essaie pas de tout résoudre. Il doit fournir à l’équipe les informations nécessaires pour reprendre rapidement la conversation, sans demander au client de tout répéter.

Ajoutez une table de file humaine dans Supabase :

create type public.handoff_status as enum (
  'open',
  'assigned',
  'resolved',
  'closed'
);

create table public.handoff_queue (
  id uuid primary key default gen_random_uuid(),
  restaurant_id uuid not null references public.restaurants(id) on delete cascade,
  customer_id uuid references public.customers(id) on delete set null,
  conversation_id uuid references public.conversations(id) on delete set null,
  reservation_id uuid references public.reservations(id) on delete set null,
  status public.handoff_status not null default 'open',
  priority text not null default 'normal',
  reason text not null,
  summary text,
  assigned_to text,
  resolved_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index handoff_queue_restaurant_status_idx
  on public.handoff_queue (restaurant_id, status, created_at asc);

create trigger set_handoff_queue_updated_at
before update on public.handoff_queue
for each row
execute function public.set_updated_at();

Lorsqu’un transfert est nécessaire, n8n doit créer une ligne dans cette table, puis envoyer une notification à l’équipe selon votre outil interne. La notification doit contenir le minimum utile : nom si connu, téléphone, résumé de la demande, créneau éventuel, motif de transfert et lien vers l’interface interne lorsque celle-ci existe réellement.

Le client doit recevoir une réponse honnête. Ne dites pas qu’un membre de l’équipe « répondra dans deux minutes » si vous ne pouvez pas tenir ce délai. Préférez une formulation adaptée à votre organisation :

Merci pour votre demande. Elle nécessite une vérification par notre équipe. Nous vous répondrons dès que possible.

Testez les scénarios difficiles avant toute mise en production

Un workflow de réservation doit être testé avec des messages incomplets, ambigus et contradictoires, pas seulement avec une demande parfaite. Conservez les résultats de test afin d’améliorer le prompt, les règles n8n et les procédures de reprise humaine.

  • « Une table ce soir ? » : vérifier que l’assistant demande le nombre de personnes et l’heure.
  • « Demain à 20 h » : vérifier que l’assistant demande le nombre de couverts et interprète la date à partir du fuseau horaire du restaurant.
  • « On est six, dehors si possible » : vérifier que la terrasse est traitée comme préférence ou contrainte selon vos règles.
  • « Je confirme » : vérifier que l’assistant ne confirme qu’une proposition active associée à la conversation.
  • « Finalement, nous serons huit » : vérifier que le cas est transféré si huit dépasse votre limite automatique.
  • « Mon enfant est allergique aux arachides » : vérifier que le système crée une demande humaine sans promettre une prise en charge.
  • « Annulez ma réservation » : vérifier que le système identifie une réservation unique avant d’annuler.
  • « Je veux changer vendredi pour samedi » : vérifier que la nouvelle disponibilité est trouvée avant de modifier l’ancienne réservation.
  • « Vous avez oublié ma réservation » : vérifier que le message est classé comme plainte ou litige et envoyé à l’équipe.
  • Message vide, audio, image ou document : vérifier que le système enregistre l’événement et demande une action adaptée au lieu d’échouer silencieusement.
  • Deux demandes simultanées sur la même table : vérifier que la contrainte PostgreSQL empêche la double affectation.
  • Réponse IA invalide ou interruption du fournisseur : vérifier que n8n ne poursuit pas vers une action de réservation et crée une alerte interne.

Testez également les cas de langage naturel : fautes de frappe, formulations familières, messages en plusieurs parties, horaires approximatifs et changements d’avis. L’objectif n’est pas d’obtenir une compréhension parfaite de chaque message, mais de garantir que le système se comporte de manière prudente lorsqu’il ne comprend pas.

Conclusion de la partie 4

Vous disposez maintenant du principe central de l’assistant : l’intelligence artificielle transforme une demande libre en intention et en données structurées, sans décider seule de la disponibilité ni de la confirmation. Supabase protège les réservations et n8n orchestre les branches de workflow, les questions complémentaires, les messages de proposition et les transferts humains.

Dans la partie 5, vous allez rendre le système exploitable au quotidien. Vous créerez les rappels, les confirmations avant service, le suivi des annulations et no-shows, les indicateurs utiles, les scénarios de reprise après erreur et les extensions possibles vers les avis clients, le Click & Collect ou la fidélisation.

Explorer les analyses sur les chatbots et agents IA

Sources et pour aller plus loin

Un assistant de réservation doit être piloté : rappels, annulations, incidents et transferts humains doivent rester visibles par l’équipe
Un assistant de réservation doit être piloté : rappels, annulations, incidents et transferts humains doivent rester visibles par l’équipe

Partie 5 : rappels, no-shows, indicateurs et exploitation quotidienne de votre assistant restaurant

Votre assistant WhatsApp sait désormais recevoir les messages, reconnaître une intention, consulter les disponibilités, créer une réservation temporaire, demander une confirmation et transmettre les cas sensibles à l’équipe. Il reste une étape essentielle : rendre le système exploitable au quotidien.

Un assistant de réservation ne se limite pas à la création d’une table dans une base de données. Il doit accompagner le client jusqu’au service, permettre une annulation claire, signaler les situations inhabituelles et fournir à l’équipe des informations utiles pour améliorer le parcours. Cette dernière partie vous aide à construire ces mécanismes sans présenter l’automatisation comme une solution autonome ou garantie.

À retenir : un rappel WhatsApp peut être utile lorsqu’il donne au client une action simple : confirmer, modifier ou annuler. Il ne doit pas devenir une pression commerciale ni entraîner l’annulation automatique d’une réservation sans règle claire, délai raisonnable et possibilité de reprise humaine.

Pourquoi les rappels et le suivi font partie du parcours de réservation

Une réservation confirmée peut évoluer avant le service. Le client peut oublier le créneau, devoir modifier le nombre de couverts, être en retard ou souhaiter annuler. Sans un parcours clair, l’équipe de salle reçoit ces informations trop tard ou doit rechercher manuellement des conversations dispersées.

Un système de rappel bien conçu apporte surtout de la visibilité. Il permet au client de signaler une modification avant son arrivée et aide l’équipe à connaître les réservations qui demandent une attention particulière. Il ne garantit pas qu’un client viendra, et il ne doit pas être utilisé comme un mécanisme de sanction automatique.

Avant de programmer vos messages, définissez les règles suivantes avec l’équipe :

  • À quel moment un rappel est envoyé pour chaque type de réservation.
  • Quels clients reçoivent un rappel : tous, uniquement les groupes, uniquement certains services ou certains créneaux.
  • Quelles réponses sont comprises automatiquement : confirmation, modification, annulation ou demande d’aide.
  • Quand une absence de réponse exige une alerte interne plutôt qu’une annulation.
  • Qui vérifie les réservations sensibles ou les réponses ambiguës.
  • Comment les annulations et no-shows sont enregistrés dans la base.

Ajoutez les données nécessaires au suivi opérationnel

Pour gérer les rappels et les événements après confirmation, ajoutez quelques colonnes à la table reservations. Ces champs vous permettront de savoir si un rappel a été envoyé, si le client a confirmé sa venue et si une intervention humaine a été nécessaire.

Dans Supabase, créez un script de migration, par exemple 05_reminders_and_operations.sql, puis exécutez les commandes suivantes dans votre projet de développement.

alter table public.reservations
add column if not exists reminder_sent_at timestamptz,
add column if not exists reminder_response_at timestamptz,
add column if not exists attendance_confirmed_at timestamptz,
add column if not exists cancelled_at timestamptz,
add column if not exists completed_at timestamptz,
add column if not exists no_show_marked_at timestamptz,
add column if not exists internal_notes text;

create type public.reminder_status as enum (
  'pending',
  'sent',
  'confirmed',
  'modified',
  'cancelled',
  'no_response',
  'failed',
  'needs_human_review'
);

create table if not exists public.reservation_reminders (
  id uuid primary key default gen_random_uuid(),
  reservation_id uuid not null references public.reservations(id) on delete cascade,
  reminder_type text not null,
  scheduled_for timestamptz not null,
  status public.reminder_status not null default 'pending',
  sent_at timestamptz,
  response_at timestamptz,
  whatsapp_message_id text,
  error_message text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (reservation_id, reminder_type)
);

create index if not exists reservation_reminders_status_scheduled_for_idx
  on public.reservation_reminders (status, scheduled_for);

create trigger set_reservation_reminders_updated_at
before update on public.reservation_reminders
for each row
execute function public.set_updated_at();

La table reservation_reminders sépare les rappels de la réservation elle-même. Cette structure évite de surcharger la table principale et permet de conserver un historique : rappel prévu, rappel envoyé, réponse du client, erreur technique, demande de modification ou transfert humain.

Dans une première version, limitez-vous à un seul rappel standard. Vous pourrez ajouter plus tard un rappel spécifique pour les groupes, les événements ou les réservations nécessitant une confirmation particulière. Multiplier les messages sans raison opérationnelle peut dégrader l’expérience client.

Définissez une stratégie de rappel avant de programmer le workflow

Le bon délai dépend du type de restaurant, du service, du délai habituel de réservation et de la politique de l’établissement. Un restaurant de quartier qui reçoit de nombreuses réservations le jour même ne suivra pas la même logique qu’un établissement gastronomique où les clients réservent plusieurs semaines à l’avance.

Au lieu d’appliquer une règle universelle, commencez par une stratégie simple et testable :

Type de réservation Exemple de rappel Action client attendue Décision en cas d’absence de réponse
Réservation standard Un rappel la veille ou le jour même selon votre organisation Confirmer, modifier ou annuler Conserver la réservation et surveiller le tableau de bord
Groupe ou événement Rappel plus anticipé selon vos conditions Valider l’effectif et les demandes particulières Transférer à un membre de l’équipe
Réservation avec demande spéciale Message de suivi adapté après vérification interne Confirmer les informations réellement validées Ne pas automatiser une promesse non vérifiée
Réservation proche du service Rappel court et opérationnel si votre politique le prévoit Signaler un retard, une modification ou une annulation Prévenir l’équipe de salle sans annuler automatiquement

Le délai de rappel doit être enregistré comme une règle de configuration, et non codé dans plusieurs nœuds n8n. Vous pouvez ajouter plus tard une table de paramètres par restaurant et par service. Pour le pilote, une constante documentée dans le workflow peut suffire, à condition de ne pas être dupliquée sans contrôle.

Créez le workflow n8n de planification des rappels

Vous pouvez programmer les rappels avec un nœud Schedule Trigger n8n. Ce nœud exécute régulièrement un workflow, par exemple toutes les quinze minutes, afin de rechercher les réservations dont le rappel doit être envoyé.

n8n permet également de choisir un comportement lorsque l’instance était indisponible à l’heure prévue. Pour un workflow de rappels, évitez une reprise massive non contrôlée après une longue interruption. Préférez un traitement limité, journalisé et vérifié, afin de ne pas envoyer plusieurs rappels tardifs ou contradictoires.

Créez un workflow nommé WA-04 — Planification des rappels de réservation.

  1. Ajoutez un nœud Schedule Trigger avec un intervalle adapté, par exemple toutes les quinze minutes pendant la phase de test.
  2. Ajoutez un nœud Supabase ou HTTP Request pour rechercher les réservations confirmées dont le rappel est prévu dans la fenêtre de traitement.
  3. Vérifiez qu’aucun rappel de même type n’a déjà été envoyé ou marqué en échec sans intervention humaine.
  4. Créez une ligne dans reservation_reminders avec le statut pending si elle n’existe pas encore.
  5. Envoyez le message WhatsApp avec un nœud HTTP Request sécurisé.
  6. Si Meta accepte l’envoi, enregistrez l’identifiant du message et modifiez le statut en sent.
  7. Si l’appel échoue, enregistrez l’erreur, limitez les tentatives et déclenchez une alerte interne si le problème persiste.

Une requête de recherche peut suivre ce principe : sélectionner les réservations dont le statut est confirmed, dont le début est proche selon votre règle, et dont aucun rappel similaire n’a été envoyé.

select
  r.id as reservation_id,
  r.restaurant_id,
  r.customer_name,
  r.customer_phone,
  r.party_size,
  r.starts_at,
  r.language_code
from public.reservations r
where r.status = 'confirmed'
  and r.starts_at > now()
  and r.starts_at <= now() + interval '24 hours'
  and not exists (
    select 1
    from public.reservation_reminders rr
    where rr.reservation_id = r.id
      and rr.reminder_type = 'standard'
      and rr.status in ('sent', 'confirmed', 'modified', 'cancelled')
  );

La période de vingt-quatre heures est un exemple. Adaptez-la à vos règles. Le workflow n8n doit ensuite filtrer la fenêtre exacte de rappel ou appeler une fonction SQL qui calcule le moment approprié selon le service, le restaurant et le type de réservation.

Utilisez un message clair et orienté action

Un rappel utile ne doit pas demander au client de rédiger une longue réponse. Il doit proposer des actions simples, par exemple confirmer, modifier ou annuler. Le format final dépendra des possibilités de votre compte WhatsApp, de vos modèles approuvés et de la configuration de votre API.

Voici un exemple de texte à adapter :

Bonjour {{prenom}}, nous vous rappelons votre réservation pour {{nombre_de_personnes}} personnes le {{date}} à {{heure}}.

Répondez :
CONFIRMER pour confirmer votre venue ;
MODIFIER si vous souhaitez changer votre réservation ;
ANNULER si vous ne pouvez pas venir.

Ne remplacez pas les variables dans le message avec des données non vérifiées. Le prénom, la date, l’heure et le nombre de couverts doivent provenir de Supabase après sélection de la réservation concernée.

Lorsque le contexte WhatsApp exige un modèle de message approuvé, créez un modèle correspondant dans votre compte Meta avant de l’utiliser en production. Ne supposez pas qu’un message texte libre sera toujours accepté, notamment pour les relances hors d’une conversation récente.

Traitez les réponses au rappel dans un workflow distinct

Les réponses au rappel sont reçues par le même point d’entrée WhatsApp créé dans la partie 3. Elles doivent toutefois être distinguées d’une nouvelle demande de réservation. L’état de conversation, la réservation active et les mots-clés de réponse permettent de router le client vers le bon scénario.

Ajoutez une branche spécifique dans votre workflow de classification ou créez un sous-workflow nommé WA-05 — Réponses aux rappels.

  • Si le client répond CONFIRMER, mettez à jour la ligne de rappel, enregistrez l’heure de réponse et ajoutez un événement de réservation.
  • Si le client répond ANNULER, identifiez la réservation liée au rappel, appelez la fonction cancel_reservation créée dans la partie 4 et confirmez l’annulation.
  • Si le client répond MODIFIER, ouvrez un état de conversation dédié et déclenchez le workflow de modification.
  • Si le message contient une autre formulation, utilisez l’IA structurée pour déterminer si le client confirme, modifie, annule ou demande une reprise humaine.
  • Si plusieurs réservations actives correspondent au même numéro, ne choisissez pas arbitrairement : demandez une précision ou transférez le cas à l’équipe.

Ajoutez une fonction PostgreSQL simple pour enregistrer la confirmation de présence sans modifier le statut global de la réservation :

create or replace function public.record_attendance_confirmation(
  p_reservation_id uuid,
  p_actor_reference text default 'whatsapp'
)
returns public.reservations
language plpgsql
security definer
set search_path = public
as $$
declare
  v_reservation public.reservations;
begin
  select *
  into v_reservation
  from public.reservations
  where id = p_reservation_id
  for update;

  if not found then
    raise exception 'Reservation not found';
  end if;

  if v_reservation.status <> 'confirmed' then
    raise exception 'Reservation is not active';
  end if;

  update public.reservations
  set
    attendance_confirmed_at = now(),
    updated_at = now(),
    updated_by = 'n8n:record_attendance_confirmation'
  where id = p_reservation_id
  returning * into v_reservation;

  insert into public.reservation_events (
    reservation_id,
    event_type,
    actor_type,
    actor_reference,
    payload
  )
  values (
    p_reservation_id,
    'updated',
    'customer',
    p_actor_reference,
    jsonb_build_object('attendance_confirmed_at', now())
  );

  return v_reservation;
end;
$$;

Cette confirmation indique que le client a répondu, mais elle ne garantit pas sa présence. Le statut completed ou no_show doit être renseigné après le service par l’équipe ou par un processus explicitement contrôlé.

Gérez les no-shows sans automatiser une sanction injustifiée

Un no-show correspond à une réservation confirmée pour laquelle le client ne s’est pas présenté, selon la définition et le délai retenus par le restaurant. Ce statut ne doit pas être déduit automatiquement parce qu’un client n’a pas répondu à un rappel.

La procédure recommandée consiste à donner à l’équipe une vue des réservations attendues, puis à lui permettre de marquer manuellement la présence, l’annulation tardive ou l’absence. Cette décision peut dépendre de la politique du restaurant, d’un éventuel retard annoncé, d’un appel téléphonique ou d’une erreur de saisie.

Ajoutez une fonction contrôlée pour marquer un no-show :

create or replace function public.mark_reservation_no_show(
  p_reservation_id uuid,
  p_actor_reference text,
  p_note text default null
)
returns public.reservations
language plpgsql
security definer
set search_path = public
as $$
declare
  v_reservation public.reservations;
begin
  select *
  into v_reservation
  from public.reservations
  where id = p_reservation_id
  for update;

  if not found then
    raise exception 'Reservation not found';
  end if;

  if v_reservation.status <> 'confirmed' then
    raise exception 'Only confirmed reservations can be marked as no-show';
  end if;

  if v_reservation.starts_at > now() then
    raise exception 'Reservation has not started yet';
  end if;

  update public.reservations
  set
    status = 'no_show',
    no_show_marked_at = now(),
    internal_notes = coalesce(internal_notes || E'\n', '') || coalesce(p_note, ''),
    updated_at = now(),
    updated_by = 'staff:' || p_actor_reference
  where id = p_reservation_id
  returning * into v_reservation;

  insert into public.reservation_events (
    reservation_id,
    event_type,
    actor_type,
    actor_reference,
    payload
  )
  values (
    p_reservation_id,
    'no_show_marked',
    'staff',
    p_actor_reference,
    jsonb_build_object('note', p_note)
  );

  return v_reservation;
end;
$$;

Cette fonction exige que l’heure de début soit passée. Vous pouvez ajouter une marge supplémentaire, par exemple un délai de retard défini par le restaurant, mais évitez de la fixer sans l’accord de l’équipe. Dans les premiers mois, privilégiez un marquage manuel accompagné d’une note courte.

Point de vigilance : ne transformez pas l’historique des no-shows en mécanisme opaque de refus automatique. Une absence peut résulter d’une erreur de réservation, d’un problème de transport, d’un malentendu ou d’une défaillance du système. Toute règle qui limiterait l’accès d’un client doit être définie, vérifiable et soumise à une décision humaine.

Créez un tableau de bord utile à l’équipe

Le bon tableau de bord ne cherche pas à prouver que l’IA a « révolutionné » le restaurant. Il doit aider l’équipe à comprendre ce qui fonctionne, ce qui demande une amélioration et où les clients rencontrent des difficultés.

Commencez avec des indicateurs simples, calculés sur une période définie. Comparez-les au fil du temps après avoir vérifié que la manière de les mesurer reste la même.

Indicateur Ce qu’il mesure Pourquoi il est utile Limite à garder en tête
Demandes WhatsApp reçues Volume de conversations entrantes Évalue la charge traitée par le canal Ne mesure pas la qualité des demandes
Demandes résolues automatiquement Conversations terminées sans transfert humain Identifie les parcours les plus automatisables Un taux élevé n’est pas utile si les réponses sont incorrectes
Demandes transférées à l’équipe Cas nécessitant une reprise humaine Révèle les zones de complexité ou de risque Un transfert peut être souhaitable pour une allergie ou un groupe
Délai de première réponse Temps entre le premier message et une réponse Suit la réactivité perçue par le client Une réponse rapide mais erronée n’est pas une amélioration
Taux de confirmation après rappel Réponses explicites aux rappels envoyés Évalue l’utilité du message et du timing Ne prouve pas la présence réelle
Annulations enregistrées Réservations annulées sur une période Aide à comprendre la disponibilité réellement libérée À interpréter selon la saison et le type de service
No-shows vérifiés Absences renseignées par l’équipe Permet d’identifier les créneaux ou parcours à examiner Doit reposer sur une procédure de marquage cohérente
Erreurs de workflow Échecs techniques ou règles bloquantes Priorise les correctifs techniques Un faible volume peut masquer un problème non détecté

Vous pouvez créer une vue Supabase pour obtenir un premier résumé quotidien. Voici un exemple indicatif :

create or replace view public.daily_reservation_metrics as
select
  restaurant_id,
  date_trunc('day', starts_at at time zone 'Europe/Paris')::date as service_date,
  count(*) filter (where status = 'confirmed') as confirmed_reservations,
  count(*) filter (where status = 'cancelled') as cancelled_reservations,
  count(*) filter (where status = 'completed') as completed_reservations,
  count(*) filter (where status = 'no_show') as no_show_reservations,
  count(*) filter (where attendance_confirmed_at is not null) as attendance_confirmations
from public.reservations
group by
  restaurant_id,
  date_trunc('day', starts_at at time zone 'Europe/Paris')::date;

Le fuseau horaire Europe/Paris est utilisé ici à titre d’exemple. Utilisez le fuseau correspondant à l’établissement ou une logique fondée sur la colonne timezone de la table restaurants si votre projet devient multi-sites.

Préparez un workflow d’erreurs et de reprise

Un workflow n8n qui échoue sans alerte peut faire perdre des réservations ou laisser des clients sans réponse. Les erreurs peuvent provenir de Meta, de Supabase, du fournisseur IA, d’un token expiré, d’un timeout réseau ou d’une règle métier qui n’a pas prévu un cas particulier.

n8n permet d’associer un workflow d’erreur à un workflow principal. Le workflow d’erreur doit commencer par un nœud Error Trigger. Il peut ensuite enregistrer les informations utiles et prévenir une personne responsable.

Créez un workflow nommé OPS-01 — Gestion des erreurs de réservation.

  1. Ajoutez un nœud Error Trigger comme premier nœud.
  2. Extrayez le nom du workflow en erreur, l’identifiant d’exécution, le nœud concerné, le message d’erreur et la date.
  3. Créez un enregistrement dans une table d’incidents Supabase ou dans votre outil interne.
  4. Classez l’erreur : temporaire, authentification, données invalides, conflit de réservation ou erreur inconnue.
  5. Pour une erreur temporaire, appliquez une stratégie de reprise limitée.
  6. Pour une erreur de données ou de logique, transférez la demande à une personne au lieu de relancer indéfiniment le workflow.
  7. Pour une erreur d’authentification, alertez le responsable technique et évitez tout nouvel envoi non contrôlé.

Ajoutez une table d’incidents à votre base :

create type public.incident_status as enum (
  'open',
  'investigating',
  'resolved',
  'ignored'
);

create table if not exists public.workflow_incidents (
  id uuid primary key default gen_random_uuid(),
  restaurant_id uuid references public.restaurants(id) on delete set null,
  workflow_name text,
  execution_reference text,
  node_name text,
  severity text not null default 'normal',
  error_message text,
  payload jsonb not null default '{}'::jsonb,
  status public.incident_status not null default 'open',
  assigned_to text,
  resolved_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index if not exists workflow_incidents_status_created_at_idx
  on public.workflow_incidents (status, created_at desc);

create trigger set_workflow_incidents_updated_at
before update on public.workflow_incidents
for each row
execute function public.set_updated_at();

Les appels réseau peuvent prévoir quelques tentatives limitées lorsqu’une erreur est manifestement temporaire. Cependant, ne configurez pas de relance sans fin. Une série de requêtes répétées peut créer des doublons, envoyer plusieurs messages ou masquer un problème de configuration plus sérieux.

Pour chaque nœud qui appelle Meta, Supabase ou un fournisseur de modèle, définissez une stratégie adaptée :

  • Activer des tentatives limitées pour les erreurs transitoires lorsque votre version n8n le permet.
  • Conserver un identifiant d’idempotence ou l’identifiant du message afin d’éviter les doubles envois.
  • Journaliser le résultat de chaque appel externe important.
  • Traiter séparément les erreurs d’authentification, les refus métier et les indisponibilités temporaires.
  • Créer une alerte humaine lorsque la réservation risque de rester sans réponse.
  • Ne jamais renvoyer au client le détail brut d’une erreur technique.

Le Schedule Trigger n8n permet également de choisir un comportement pour les exécutions manquées. Cette option doit être paramétrée avec prudence : après une indisponibilité, un rappel envoyé plusieurs heures trop tard peut être plus nuisible qu’utile. Documentez le comportement choisi et testez-le lors d’un incident simulé.

Décidez entre n8n Schedule Trigger et Supabase Cron

Vous pouvez planifier les rappels avec n8n ou avec Supabase Cron. Les deux approches sont possibles, mais elles ne remplissent pas exactement le même rôle.

Approche Cas d’usage adapté Avantage principal Point de vigilance
n8n Schedule Trigger Rappel nécessitant des appels à WhatsApp, à l’IA ou à plusieurs services Le workflow visuel regroupe recherche, décision, envoi et journalisation Vous devez surveiller les exécutions manquées et les erreurs réseau
Supabase Cron Nettoyage de données, expiration de réservations temporaires ou calculs SQL récurrents La logique proche des données reste dans PostgreSQL Les appels externes et le traitement métier complexe restent souvent plus lisibles dans n8n
Approche hybride Expiration en base et messages orchestrés dans n8n Chaque outil conserve une responsabilité claire Les règles doivent être documentées pour éviter les doublons

Pour ce tutoriel, l’approche hybride est recommandée. Supabase Cron peut expirer régulièrement les réservations temporaires et effectuer des contrôles de base. n8n peut ensuite gérer les messages WhatsApp, les règles conversationnelles, les tentatives limitées et les alertes humaines.

Supabase Cron s’appuie sur l’extension pg_cron. Vous pouvez planifier des tâches depuis SQL ou depuis les outils proposés par Supabase selon votre environnement. Avant d’activer un job, testez sa requête manuellement et vérifiez les journaux d’exécution.

Testez le système comme une équipe de restaurant, pas comme une démonstration

Un test réussi ne se limite pas à l’envoi d’un premier message WhatsApp. Vous devez simuler les situations qui se produisent avant, pendant et après un service : clients qui répondent tard, demandes de dernière minute, erreurs réseau, modifications et cas sensibles.

  • Créez une réservation de test confirmée et vérifiez qu’un seul rappel est programmé.
  • Simulez une confirmation client et contrôlez la mise à jour de la réservation, du rappel et des événements.
  • Simulez une annulation et vérifiez que la table redevient disponible selon les règles définies.
  • Simulez une demande de modification après un rappel et vérifiez que l’ancienne réservation n’est pas libérée avant validation de la nouvelle.
  • Simulez une absence de réponse et vérifiez qu’aucune annulation automatique ne se produit sans règle explicite.
  • Marquez manuellement une réservation comme no-show après l’heure de service et contrôlez la trace dans les événements.
  • Coupez temporairement l’accès à Meta ou Supabase dans un environnement de test et vérifiez la création d’un incident.
  • Testez une erreur de token et assurez-vous qu’elle déclenche une alerte interne sans exposer le secret.
  • Testez une exécution planifiée manquée et vérifiez le comportement choisi dans n8n.
  • Vérifiez que les rapports quotidiens comptent les statuts de manière cohérente.
  • Faites relire les messages envoyés au client par une personne de salle, de direction ou de relation client.
  • Demandez à l’équipe de reprendre plusieurs cas transférés afin de vérifier que les informations transmises sont suffisantes.

Conservez une fiche de recette simple. Pour chaque scénario, notez la date, le résultat attendu, le résultat observé, les éventuels messages envoyés et la correction apportée. Cette discipline réduit le risque de modifier un workflow sans savoir quel problème vous cherchez à résoudre.

Extensions possibles après validation du pilote

Une fois le flux de réservation stabilisé, vous pouvez envisager des extensions. Ne les activez pas toutes en même temps. Chaque extension modifie les données, les messages, les droits d’accès ou les responsabilités opérationnelles.

Gestion des avis clients

Vous pouvez centraliser les nouveaux avis reçus, demander à l’IA de préparer un résumé et proposer une réponse à l’équipe. Cependant, la publication automatique d’une réponse n’est pas recommandée dans la première version. Une plainte, une critique sur une allergie ou une demande de remboursement exige un examen humain.

Click & Collect et commandes

Un assistant peut également répondre à des questions sur les horaires de retrait, récupérer une commande et transmettre les détails à un système de production. Cette extension exige une source de vérité distincte pour les produits, les stocks, les prix, les allergènes et les créneaux de retrait. Ne réutilisez pas simplement le workflow de réservation de table sans adapter les règles.

Acompte, garantie ou paiement sur place

Un restaurant peut, selon son modèle, souhaiter demander un acompte ou une garantie pour certains groupes ou événements. Cette fonctionnalité doit être traitée comme un projet séparé : parcours de paiement, conditions d’annulation, politiques de remboursement, conformité, sécurité et intégration avec l’outil de paiement doivent être vérifiés avant de l’activer.

Fidélisation et marketing

Une réservation ne vaut pas automatiquement un consentement marketing. Si vous souhaitez proposer une inscription à une newsletter, un programme de fidélité ou des offres événementielles, créez un parcours distinct avec un consentement explicite, des informations claires et une possibilité simple de se désinscrire.

Conseil de déploiement : stabilisez d’abord le parcours le plus simple : demande de réservation, disponibilité, confirmation, annulation et transfert humain. Ajoutez ensuite une extension à la fois, avec des tests, des indicateurs et un responsable identifié.

Conclusion du dossier

Vous disposez maintenant d’une architecture complète pour concevoir un assistant WhatsApp de réservation restaurant avec n8n, Supabase et IA. Le système repose sur une idée simple : l’IA comprend le message, n8n orchestre le parcours, Supabase vérifie les règles et l’équipe conserve le contrôle des situations sensibles.

Vous avez défini les règles métier, créé une base relationnelle, protégé les réservations contre les chevauchements, connecté WhatsApp à n8n, structuré les sorties de l’IA, organisé les réservations temporaires, les confirmations, les annulations et la reprise humaine. Vous avez aussi prévu les rappels, les incidents et les indicateurs nécessaires à une exploitation quotidienne.

La prochaine étape réaliste consiste à lancer un pilote limité : un établissement, un service, un nombre réduit de créneaux et une équipe clairement identifiée pour reprendre les exceptions. Observez les conversations, mesurez les erreurs, améliorez les règles et élargissez le périmètre uniquement lorsque le fonctionnement est compris et maîtrisé.

Explorer les analyses sur l’IA et l’e-commerce

Questions fréquentes sur un assistant WhatsApp de réservation restaurant

Faut-il savoir coder pour créer un assistant WhatsApp de réservation ?

Pas nécessairement pour construire un premier prototype avec n8n, Supabase et des outils no-code ou low-code. Vous devez toutefois comprendre quelques notions essentielles : webhook, base de données, API, champs de données, clés d’accès et règles métier. La difficulté ne vient pas seulement du code : elle dépend surtout de votre capacité à définir clairement les horaires, les capacités, les règles de confirmation et les cas qui doivent être traités par l’équipe.

Peut-on utiliser WhatsApp pour recevoir des demandes de réservation ?

Oui. WhatsApp Business Cloud API permet de recevoir des événements et des messages entrants via un webhook configuré par l’entreprise. Votre workflow n8n peut ensuite enregistrer la demande, identifier le client, demander les informations manquantes et vérifier les disponibilités dans votre base de données. Vous devez néanmoins respecter les règles applicables à votre compte WhatsApp Business, notamment pour les messages envoyés hors d’une conversation récente.

L’assistant peut-il confirmer une table automatiquement ?

Oui, mais seulement si toutes les conditions sont vérifiées par des règles déterministes : créneau ouvert, capacité suffisante, table disponible, nombre de couverts compatible, données client suffisantes et absence de demande sensible. L’intelligence artificielle peut comprendre la demande, mais Supabase et les règles métier doivent vérifier la disponibilité avant toute confirmation. Les groupes importants, allergies, privatisations ou demandes ambiguës doivent être transférés à une personne.

Pourquoi ne pas laisser l’IA décider directement des disponibilités ?

Un modèle d’intelligence artificielle peut produire une réponse plausible sans connaître l’état réel de la salle, les réservations reçues quelques secondes plus tôt, les fermetures exceptionnelles ou les changements effectués par l’équipe. La disponibilité doit donc être calculée dans une base de données à partir de règles contrôlées. L’IA sert à interpréter les messages et à extraire les informations utiles, pas à inventer ou valider un créneau.

Supabase est-il obligatoire pour ce type de projet ?

Non. Supabase est utilisé dans ce dossier parce qu’il fournit une base PostgreSQL, des fonctions de base de données et des mécanismes de contrôle d’accès utiles pour un projet de réservation. Vous pouvez utiliser une autre base PostgreSQL, un back-end personnalisé ou un logiciel de réservation existant. L’exigence principale est de disposer d’une source de vérité unique pour les créneaux, les capacités et les réservations.

Peut-on commencer avec Google Sheets au lieu d’une base PostgreSQL ?

Oui, pour une démonstration limitée ou un test interne. En revanche, un tableur devient plus fragile lorsque plusieurs demandes arrivent simultanément, lorsque vous devez gérer des tables, des créneaux qui se chevauchent, des annulations ou plusieurs membres de l’équipe. Une base relationnelle convient davantage à un usage quotidien, car elle peut appliquer des contraintes et conserver un historique structuré.

Comment éviter les doubles réservations ?

La prévention doit fonctionner à plusieurs niveaux. Le workflow peut rechercher les disponibilités avant de proposer un créneau. La base PostgreSQL doit ensuite empêcher deux affectations incompatibles sur la même table et la même période. Enfin, les réservations temporaires doivent expirer automatiquement si le client ne confirme pas. Cette combinaison est plus robuste qu’un simple contrôle réalisé dans n8n avant l’écriture en base.

Qu’est-ce qu’une réservation temporaire ou hold ?

Une réservation temporaire bloque brièvement une table pendant qu’un client confirme une proposition. Elle évite qu’un même créneau soit proposé à plusieurs personnes au même moment. Elle doit toutefois avoir une durée limitée et une date d’expiration claire. Si le client ne confirme pas dans le délai prévu, le blocage expire et la table redevient disponible selon les règles du restaurant.

Que doit faire l’assistant lorsqu’un client demande une table en terrasse ?

La terrasse doit être traitée selon la règle définie par le restaurant. Si elle constitue une exigence impérative, le workflow doit rechercher uniquement les tables extérieures disponibles. Si elle constitue une préférence, l’assistant peut l’enregistrer sans la promettre. La formulation envoyée au client doit rester précise : une préférence de placement n’est pas une garantie, notamment lorsque la météo ou l’organisation de la salle peuvent changer.

Comment gérer les allergies et les demandes alimentaires particulières ?

L’assistant peut enregistrer la demande et alerter l’équipe, mais il ne doit jamais confirmer automatiquement qu’une allergie ou une restriction médicale peut être prise en charge. La réponse dépend de la carte du moment, de la cuisine, des procédures de préparation, du risque de contamination croisée et de la capacité réelle de l’établissement. Les mentions d’allergie sévère, d’anaphylaxie ou de besoin médical doivent déclencher une reprise humaine.

À partir de combien de personnes faut-il transférer la demande à l’équipe ?

Cela dépend de l’organisation du restaurant, des tables combinables, du menu, du personnel et des règles de groupe. Dans une première version, définissez une limite prudente, par exemple au-delà de six personnes, puis transférez les demandes plus importantes. Cette règle peut être ajustée après observation des demandes réelles et validation de la capacité du restaurant à traiter automatiquement certains groupes.

Peut-on modifier une réservation directement dans WhatsApp ?

Oui, mais une modification doit être traitée avec prudence. Le système doit identifier la bonne réservation, vérifier la nouvelle disponibilité, conserver l’ancienne réservation tant que la nouvelle n’est pas validée, puis journaliser les changements. Si plusieurs réservations correspondent au même numéro de téléphone ou si le message est ambigu, l’assistant doit demander une précision ou transférer la demande à une personne.

L’assistant peut-il annuler une réservation automatiquement ?

Il peut traiter une annulation lorsqu’il identifie sans ambiguïté la réservation concernée et que cette action respecte la politique du restaurant. La réservation ne doit pas être supprimée : son statut doit devenir « annulée » afin de conserver une trace. Les cas comprenant un acompte, une demande de remboursement, un litige ou une annulation tardive doivent être transférés à l’équipe.

Que se passe-t-il si le client ne répond pas au rappel WhatsApp ?

L’absence de réponse ne doit pas être assimilée automatiquement à une annulation ou à un no-show. Le restaurant peut conserver la réservation, déclencher une alerte interne ou appliquer une procédure définie pour certains types de réservations. Le choix dépend de la politique de l’établissement et doit être clairement documenté. L’objectif d’un rappel est de faciliter la confirmation ou l’annulation, pas de pénaliser automatiquement le client.

Comment déclarer un no-show ?

Un no-show doit être vérifié par l’équipe après l’horaire de réservation et selon une procédure claire. Il ne doit pas être déduit uniquement du silence du client ou de l’absence de réponse à un rappel. Une personne habilitée doit pouvoir marquer la réservation comme absence, ajouter une note si nécessaire et corriger la décision en cas d’erreur de saisie, de retard signalé ou de problème côté restaurant.

Peut-on empêcher automatiquement un ancien no-show de réserver ?

Il est déconseillé de mettre en place un blocage automatique à partir d’un historique d’absence sans règles explicites, vérifiables et contrôlées par une personne. Un no-show peut avoir plusieurs causes : erreur de réservation, problème de transport, malentendu, erreur technique ou difficulté personnelle. Si le restaurant souhaite appliquer une politique spécifique, elle doit être clairement définie, proportionnée et validée selon son contexte juridique et commercial.

Comment protéger les données des clients ?

Collectez uniquement les informations nécessaires à la réservation et au suivi opérationnel : par exemple le nom, le numéro WhatsApp, la date, l’heure, le nombre de couverts et les demandes utiles. Limitez les accès, activez des règles de sécurité adaptées dans Supabase, ne placez jamais les clés privilégiées dans une interface publique et définissez une durée de conservation cohérente. Les tables et vues exposées via une API doivent associer des droits explicites et des politiques Row Level Security adaptées.

Le numéro WhatsApp d’un client autorise-t-il l’envoi d’offres marketing ?

Non. Une demande de réservation ne vaut pas automatiquement consentement à recevoir des communications marketing. Si vous souhaitez proposer une newsletter, un programme de fidélité ou des offres, prévoyez un parcours distinct avec une information claire, un consentement adapté à votre contexte et une possibilité simple de se désinscrire ou de retirer son accord.

Faut-il répondre automatiquement aux avis Google avec l’IA ?

Il est préférable de commencer par une assistance à la rédaction. L’IA peut résumer un avis et proposer un brouillon de réponse, mais une personne doit vérifier le ton, les faits et les engagements formulés avant publication. Cette précaution est particulièrement importante pour les critiques liées à une allergie, un litige, une facture, une discrimination ou une expérience client sensible.

Quels indicateurs faut-il suivre au lancement ?

Suivez d’abord des indicateurs opérationnels : volume de demandes reçues, nombre de demandes résolues automatiquement, transferts à l’équipe, délai de première réponse, confirmations après rappel, annulations, no-shows vérifiés et erreurs de workflow. Ces données permettent d’identifier les parcours à améliorer. Elles ne prouvent pas à elles seules un gain de chiffre d’affaires ou une réduction des absences.

Comment savoir si l’assistant fonctionne réellement ?

Ne vous limitez pas au nombre de messages envoyés. Examinez la qualité des informations collectées, les erreurs de disponibilité évitées, le taux de demandes reprises par l’équipe, les conversations abandonnées, les motifs de transfert humain et les retours des personnes en salle. Un assistant est utile lorsqu’il réduit une friction sans créer davantage de corrections, d’ambiguïtés ou de demandes non traitées.

Que faire si WhatsApp, n8n ou Supabase devient indisponible ?

Prévoyez une procédure de reprise avant le lancement : journalisation des erreurs, alertes internes, tentatives limitées pour les erreurs temporaires et canal manuel de secours. Si le système ne peut pas vérifier une disponibilité ou enregistrer une réservation, il ne doit pas confirmer le client. Il doit créer une alerte pour l’équipe et, lorsque cela est possible, informer le client qu’une vérification manuelle est nécessaire.

Quelle est la meilleure façon de lancer le projet ?

Commencez avec un pilote limité : un seul établissement, un nombre réduit de créneaux, une langue, des groupes restreints et un responsable clairement identifié pour les reprises humaines. Testez les demandes simples, les erreurs, les annulations et les cas sensibles avec des données fictives. Analysez ensuite les conversations réelles de manière encadrée, corrigez les règles et élargissez progressivement le périmètre.

Sources et pour aller plus loin

Afficher plus

Damien LADURELLE

Après un long moment en tant que gérant d'une agence web & communication à Lille, j'ai rejoint les rangs de l'entreprise au poste de Directeur Marketing et Communication dans une holding. Aujourd'hui, j'accompagne TPE & PME dans leur croissance digitale grâce notamment à l'aide des dernières innovations technologiques. Toujours à la recherche de nouvelles façons de se démarquer, d'innover, de dépasser les barrières du "casual".

Articles similaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Bouton retour en haut de la page