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.
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 :
- Quels jours et quels services peuvent être réservés via WhatsApp ?
- Combien de couverts maximum pouvez-vous accepter par créneau ?
- Quelle durée moyenne associez-vous à une table de deux, quatre, six ou huit personnes ?
- À partir de combien de personnes faut-il obligatoirement une validation humaine ?
- 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 ?
- Quelles conditions d’annulation ou de retard doivent être communiquées au client ?
- 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.
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 :
- Le client envoie un premier message sur WhatsApp.
- 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.
- 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.
- Le workflow vérifie que les données reçues sont complètes et dans un format utilisable.
- Supabase calcule la disponibilité réelle selon les règles du restaurant.
- Le système propose un ou plusieurs créneaux, ou indique qu’une vérification humaine est nécessaire.
- Le client confirme explicitement le créneau proposé.
- Le workflow crée une réservation confirmée, enregistre l’événement et envoie un message récapitulatif.
- Avant le service, le système envoie un rappel selon la politique définie.
- 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.
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 ?
L’assistant peut-il confirmer une réservation automatiquement ?
Faut-il obligatoirement utiliser Supabase ?
Peut-on commencer avec Google Sheets ?
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-commerceSources et pour aller plus loin
- Meta for Developers — WhatsApp Cloud API : présentation de l’API officielle et des fonctionnalités de messagerie.
- Meta for Developers — WhatsApp Webhooks : fonctionnement des notifications envoyées par webhook.
- n8n Docs — Workflow development with Webhook : distinction entre URL de test et URL de production dans n8n.
- Supabase Docs — Database : présentation de la base PostgreSQL et de la logique exécutée au niveau de la base.
- Supabase Docs — Row Level Security : principes de contrôle d’accès granulaire au niveau des lignes.
Partie 2 : créer la base Supabase pour gérer les réservations sans double créneau

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.
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.
- Créez un nouveau projet Supabase dédié au développement.
- Choisissez une région cohérente avec vos contraintes opérationnelles et de données.
- Conservez les identifiants du projet dans un gestionnaire de secrets ou dans les identifiants sécurisés de n8n.
- Ouvrez l’éditeur SQL de Supabase.
- Créez un nouveau script nommé, par exemple,
01_schema_reservations.sql. - 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.
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.
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_slotset 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-commerceSources et pour aller plus loin
- PostgreSQL — Range Types : documentation des plages temporelles et des contraintes d’exclusion utilisées pour empêcher les chevauchements.
- Supabase — Database : présentation de la base PostgreSQL et des mécanismes disponibles dans Supabase.
- Supabase — Row Level Security : principes de contrôle d’accès granulaire au niveau des lignes.
- Supabase — Database Functions : création et utilisation de fonctions PostgreSQL.
- Supabase — pg_cron : planification de tâches récurrentes dans PostgreSQL.

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.
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.
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.
- Ajoutez un nœud Webhook.
- Choisissez la méthode HTTP
GET. - Définissez un chemin unique, par exemple
whatsapp-reservations. - Dans le paramètre de réponse du Webhook, sélectionnez une réponse via le nœud Respond to Webhook.
- Ajoutez un nœud IF pour comparer le paramètre
hub.verify_tokenreçu avec votre jeton de vérification stocké de façon sécurisée. - Dans la branche valide, ajoutez un nœud Respond to Webhook qui renvoie la valeur de
hub.challengeavec un code HTTP 200. - 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.
- Ajoutez un nœud Webhook en méthode
POST. - Configurez une réponse rapide avec un nœud Respond to Webhook qui renvoie un statut HTTP 200.
- Ajoutez un nœud Code ou Set pour extraire les objets utiles du payload.
- Ajoutez un nœud IF ou Switch pour distinguer les messages entrants des statuts techniques.
- Pour un message entrant, normalisez le téléphone, l’identifiant du message, le type de contenu, le texte et l’horodatage.
- 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 IASources et pour aller plus loin
- Meta for Developers — WhatsApp Cloud API : présentation de l’API officielle de messagerie WhatsApp Business.
- Meta for Developers — WhatsApp Webhooks : fonctionnement des notifications et payloads envoyés vers un webhook.
- Meta for Developers — WhatsApp Text Messages : endpoint et structure des requêtes utilisées pour envoyer un message texte.
- n8n Docs — Webhook workflow development : distinction entre URL de test et URL de production.
- n8n Docs — Respond to Webhook node : configuration d’une réponse contrôlée à un webhook.

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.
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 :
- Recevoir le message normalisé, le client et la conversation active.
- Vérifier que le message est de type texte et qu’il contient un contenu exploitable.
- Récupérer le contexte de conversation limité depuis Supabase.
- Envoyer le message et le contexte au modèle avec le prompt système.
- Valider que la sortie respecte le schéma attendu.
- Vérifier les règles de sécurité indépendamment de l’IA, par exemple avec une liste de mots-clés sensibles.
- 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 :
- Le message est classé avec l’intention
new_reservationouavailability_request. - n8n vérifie les champs requis : date, heure, nombre de personnes et nom si votre règle l’exige avant la confirmation.
- Si une donnée manque, n8n envoie la question proposée ou une formulation validée, puis met à jour
current_stepdans la conversation. - 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.
- Si les données sont complètes, n8n calcule l’heure de fin selon les règles de service et appelle
find_available_tables. - Si aucune table n’est disponible, n8n recherche des alternatives ou demande à l’équipe de vérifier une solution.
- Si une table est disponible, n8n appelle
create_reservation_hold. - n8n envoie une proposition claire au client avec le créneau, le nombre de couverts et une demande de confirmation.
- Lorsque le client confirme, l’IA classe le message en
confirm_proposalet n8n appelleconfirm_reservation_hold. - 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 :
- Identifier la réservation active associée au client.
- Vérifier que la demande de modification est suffisamment précise.
- Si nécessaire, demander la nouvelle date, heure ou taille de groupe.
- Rechercher une disponibilité pour le nouveau créneau avant de libérer l’ancien.
- Créer un blocage temporaire sur la nouvelle table ou le nouveau créneau.
- Demander au client de confirmer la modification proposée.
- Après confirmation, mettre à jour la réservation et l’affectation de table dans une opération contrôlée.
- 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 IASources et pour aller plus loin
- OpenAI Developers — Structured model outputs : principes des sorties contraintes par JSON Schema et différences avec le simple JSON valide.
- OpenAI — Introducing Structured Outputs in the API : explication de l’usage de schémas stricts et de la gestion des refus.
- n8n Docs — Tools Agent : formats de sortie structurés, outils et validation humaine dans un workflow n8n.
- Anthropic Docs — Tool use : définition des paramètres d’outil à l’aide d’un schéma JSON.
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.
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 :
- Recevoir le message normalisé, le client et la conversation active.
- Vérifier que le message est de type texte et qu’il contient un contenu exploitable.
- Récupérer le contexte de conversation limité depuis Supabase.
- Envoyer le message et le contexte au modèle avec le prompt système.
- Valider que la sortie respecte le schéma attendu.
- Vérifier les règles de sécurité indépendamment de l’IA, par exemple avec une liste de mots-clés sensibles.
- 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 :
- Le message est classé avec l’intention
new_reservationouavailability_request. - n8n vérifie les champs requis : date, heure, nombre de personnes et nom si votre règle l’exige avant la confirmation.
- Si une donnée manque, n8n envoie la question proposée ou une formulation validée, puis met à jour
current_stepdans la conversation. - 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.
- Si les données sont complètes, n8n calcule l’heure de fin selon les règles de service et appelle
find_available_tables. - Si aucune table n’est disponible, n8n recherche des alternatives ou demande à l’équipe de vérifier une solution.
- Si une table est disponible, n8n appelle
create_reservation_hold. - n8n envoie une proposition claire au client avec le créneau, le nombre de couverts et une demande de confirmation.
- Lorsque le client confirme, l’IA classe le message en
confirm_proposalet n8n appelleconfirm_reservation_hold. - 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 :
- Identifier la réservation active associée au client.
- Vérifier que la demande de modification est suffisamment précise.
- Si nécessaire, demander la nouvelle date, heure ou taille de groupe.
- Rechercher une disponibilité pour le nouveau créneau avant de libérer l’ancien.
- Créer un blocage temporaire sur la nouvelle table ou le nouveau créneau.
- Demander au client de confirmer la modification proposée.
- Après confirmation, mettre à jour la réservation et l’affectation de table dans une opération contrôlée.
- 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 IASources et pour aller plus loin
- OpenAI Developers — Structured model outputs : principes des sorties contraintes par JSON Schema et différences avec le simple JSON valide.
- OpenAI — Introducing Structured Outputs in the API : explication de l’usage de schémas stricts et de la gestion des refus.
- n8n Docs — Tools Agent : formats de sortie structurés, outils et validation humaine dans un workflow n8n.
- Anthropic Docs — Tool use : définition des paramètres d’outil à l’aide d’un schéma JSON.

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.
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.
- Ajoutez un nœud Schedule Trigger avec un intervalle adapté, par exemple toutes les quinze minutes pendant la phase de test.
- 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.
- Vérifiez qu’aucun rappel de même type n’a déjà été envoyé ou marqué en échec sans intervention humaine.
- Créez une ligne dans
reservation_remindersavec le statutpendingsi elle n’existe pas encore. - Envoyez le message WhatsApp avec un nœud HTTP Request sécurisé.
- Si Meta accepte l’envoi, enregistrez l’identifiant du message et modifiez le statut en
sent. - 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 fonctioncancel_reservationcréé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.
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.
- Ajoutez un nœud Error Trigger comme premier nœud.
- Extrayez le nom du workflow en erreur, l’identifiant d’exécution, le nœud concerné, le message d’erreur et la date.
- Créez un enregistrement dans une table d’incidents Supabase ou dans votre outil interne.
- Classez l’erreur : temporaire, authentification, données invalides, conflit de réservation ou erreur inconnue.
- Pour une erreur temporaire, appliquez une stratégie de reprise limitée.
- Pour une erreur de données ou de logique, transférez la demande à une personne au lieu de relancer indéfiniment le workflow.
- 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.
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-commerceQuestions fréquentes sur un assistant WhatsApp de réservation restaurant
Faut-il savoir coder pour créer un assistant WhatsApp de réservation ?
Peut-on utiliser WhatsApp pour recevoir des demandes de réservation ?
L’assistant peut-il confirmer une table automatiquement ?
Pourquoi ne pas laisser l’IA décider directement des disponibilités ?
Supabase est-il obligatoire pour ce type de projet ?
Peut-on commencer avec Google Sheets au lieu d’une base PostgreSQL ?
Comment éviter les doubles réservations ?
Qu’est-ce qu’une réservation temporaire ou hold ?
Que doit faire l’assistant lorsqu’un client demande une table en terrasse ?
Comment gérer les allergies et les demandes alimentaires particulières ?
À partir de combien de personnes faut-il transférer la demande à l’équipe ?
Peut-on modifier une réservation directement dans WhatsApp ?
L’assistant peut-il annuler une réservation automatiquement ?
Que se passe-t-il si le client ne répond pas au rappel WhatsApp ?
Comment déclarer un no-show ?
Peut-on empêcher automatiquement un ancien no-show de réserver ?
Comment protéger les données des clients ?
Le numéro WhatsApp d’un client autorise-t-il l’envoi d’offres marketing ?
Faut-il répondre automatiquement aux avis Google avec l’IA ?
Quels indicateurs faut-il suivre au lancement ?
Comment savoir si l’assistant fonctionne réellement ?
Que faire si WhatsApp, n8n ou Supabase devient indisponible ?
Quelle est la meilleure façon de lancer le projet ?
Sources et pour aller plus loin
- Meta for Developers — WhatsApp Cloud API Guides : guides officiels sur l’envoi de messages et les formats disponibles.
- Meta for Developers — WhatsApp Message Templates : documentation relative aux modèles de messages et à leurs usages.
- n8n Docs — Handle errors gracefully : mise en place d’un workflow d’erreur avec Error Trigger.
- n8n Docs — Schedule Trigger : programmation de workflows et gestion des exécutions manquées.
- Supabase Docs — Cron : planification de tâches récurrentes avec pg_cron.
- Supabase Docs — Webhook debugging guide : pistes de diagnostic et journalisation des problèmes liés aux webhooks.



