Construire un tunnel de réservation
Un tunnel de réservation est la séquence que parcourt un client : choisir une
date, choisir une heure, dire qui il est, obtenir une confirmation. Quatre points
de terminaison la portent — GET /v1/configuration,
GET /v1/availability/months, GET /v1/availability et
POST /v1/reservations — et tout le reste de cette page traite des réponses que
ces quatre-là vous donnent quand la réservation ne passe pas du premier coup.
D’abord : il vous faut un serveur
Section intitulée « D’abord : il vous faut un serveur »Toutes les informations d’authentification émises par cette API sont secrètes et
api.useservice.app n’accorde aucun accès cross-origin à une origine que vous
contrôlez. Service ouvre CORS pour /v1/public/* et pour rien d’autre, et ce
sont là les points de terminaison non authentifiés du widget de réservation, sur
l’hôte de l’application, et non une partie de cette API. Une clé sk_live_… ne
peut donc pas fonctionner depuis une page livrée aux clients : ni derrière une
étape de build, ni dans un service worker, ni avec un en-tête de proxy. Voir
Aucune clé utilisable dans un navigateur.
Reconstruire le tunnel, c’est reconstruire le tunnel et faire tourner un serveur.
Le navigateur du client parle à votre backend ; votre backend détient la clé et appelle Service. Si vous avez déjà un backend, c’est un petit ajout. Sinon, c’est une nouvelle brique d’infrastructure à construire et à exploiter.
Si vous voulez le parcours de réservation sur votre site sans aucun backend, intégrez plutôt le widget de réservation. Il ne demande aucune clé et c’est le même tunnel.
Une clé, un restaurant
Section intitulée « Une clé, un restaurant »Une clé d’API appartient à un seul restaurant. Il n’existe pas de clé de groupe
ni de paramètre restaurant_id : le restaurant est résolu à partir de la clé,
ce qui fait de GET /v1/configuration le seul moyen de confirmer à quel
restaurant vous parlez.
Un groupe de dix établissements, ce sont dix clés, créées dix fois dans dix back-offices, et chacune de vos requêtes porte sur exactement l’un d’eux. Votre serveur choisit la clé avant de choisir le point de terminaison. Construisez dès le premier jour la correspondance entre votre propre identifiant de site et une clé : la rétro-adapter à l’arrivée d’un deuxième restaurant oblige à reprendre chaque appel.
Portées nécessaires
Section intitulée « Portées nécessaires »| Portée | Pourquoi le tunnel en a besoin |
|---|---|
reservations:read |
La configuration, les sections et les deux points de terminaison de disponibilité. |
reservations:write |
La création. |
guests:read |
Uniquement pour ?expand=modification_token sur POST /v1/reservations, voir Terminer le travail. Les coordonnées et les notes d’une réservation l’exigent aussi, y compris sur celles que le tunnel a prises lui-même : voir Données client d’une réservation. |
reservations:write:override |
Uniquement si le tunnel a le droit de réserver au-delà d’une règle posée par le restaurant. Un tunnel ouvert au public ne l’a en général pas. |
Erreurs fait référence sur ce que chaque portée
accorde et sur la forme d’un refus. L’accès à l’API relève de l’offre Premium :
une clé sur un restaurant qui ne l’a pas est valide et refusée quand même, avec
plan_required.
Lire la politique une fois
Section intitulée « Lire la politique une fois »curl "https://api.useservice.app/v1/configuration" \ -H "Authorization: Bearer $SERVICE_API_KEY"C’est l’amorçage. Récupérez-le au démarrage, gardez-le et revalidez-le avec
If-None-Match plutôt que de le relire à chaque client. Il change quand un
gestionnaire change un réglage, pas à chaque requête.
Quatre champs décident du comportement de tout le reste du tunnel :
timezone— chaqueservice_dateet chaqueHH:MMde cette API est une heure murale dans le fuseau du restaurant. Jamais UTC, jamais celui du navigateur. Convertissez à la frontière de votre système et gardez la journée du restaurant à l’intérieur.min_party_size/max_party_size/max_advance_days— la politique de réservation en ligne du restaurant. En dehors, la création est refusée avecparty_size_too_small,party_size_too_largeouadvance_window_exceeded. À l’intérieur, un nombre de couverts peut n’avoir aucune table : la politique peut dire 1 à 20 dans un restaurant dont les tables accueillent 2 à 8 personnes. Construisez le champ du nombre de couverts à partir departy_sizessurGET /v1/availability/months, ci-dessous, qui liste les nombres de couverts plaçables.last_service_datevautcurrent_date + max_advance_days, pré-calculé pour borner un calendrier par une comparaison de chaînes.guest_languages— les langues dans lesquelles ce restaurant entretient ses textes destinés aux clients. Ce tableau borne?locale=ci-dessous et il borne la langue dans laquelle un client peut être écrit.default_cutoff_minutes— la valeur par défaut de la maison pour la fermeture des réservations en ligne, et non la valeur appliquée à un service donné. Celle-ci est propre à chaque service et arrive avec les disponibilités.
Proposer le calendrier, puis les heures
Section intitulée « Proposer le calendrier, puis les heures »GET /v1/availability/months?month=2026-10 renvoie les jours qui ont de la
disponibilité, pour tous les nombres de couverts à la fois. Il ne prend ni
nombre de couverts ni section : une seule réponse alimente le sélecteur de mois
entier et reste valable pendant que le client change d’avis sur le nombre de
convives. Chaque jour porte les nombres de couverts qui passent, ou la raison
unique pour laquelle il n’est pas proposé. Les raisons, dans l’ordre où Service
les vérifie, sont sur
L’objet disponibilité.
GET /v1/availability?service_date=2026-10-04&party_size=4 répond les
heures, pour une date et un nombre de couverts, service par service.
curl -G "https://api.useservice.app/v1/availability" \ -H "Authorization: Bearer $SERVICE_API_KEY" \ --data-urlencode "service_date=2026-10-04" \ --data-urlencode "party_size=4" \ --data-urlencode "locale=fr"Les deux montrent ce qui était vendable au moment où vous les avez lues. Ni l’une ni l’autre ne réserve quoi que ce soit. La création revalide chaque plafond et chaque table sous verrou, ce qui explique qu’un tunnel ait autant besoin du chemin de refus plus bas que de celui-ci.
Envoyez un start_time pris dans slots[], et ne le calculez jamais. Service
applique les horaires d’arrivée du service à la création : une heure située
entre deux horaires d’arrivée, ou après le dernier, est refusée avec
start_time_off_grid alors même que le
service est ouvert.
Par service, six champs changent ce que vous dessinez :
| Champ | Ce que le tunnel en fait |
|---|---|
slots[] |
Les heures réservables. available_covers: null signifie aucun plafond configuré : illimité, et non zéro. |
unavailable_slots[] |
Des heures que ce service vend et que ce groupe ne peut pas prendre pour l’instant. Affichez-les grisées. Masquez-les et la page paraît cassée aux heures de pointe, alors que le client voit bien que le restaurant est ouvert. |
effective_cutoff_minutes |
Le moment où la réservation en ligne ferme pour ce service à cette date, résolu à travers la cascade de règles du restaurant. C’est le compte à rebours à montrer au client. La valeur de la configuration est la valeur par défaut de la maison et peut être fausse pour le service que vous avez devant vous. |
booking_closed |
La réservation en ligne pour ce service à cette date est terminée : sa limite de réservation est passée, ou son dernier horaire d’arrivée l’est. Ses slots sont vides parce que sa réservation est close, pas parce qu’il est plein. Retirez le service de la liste et ne l’affichez jamais comme complet. |
display_only |
Le service mérite d’être affiché et ne prend aucune réservation en ligne. Affichez son guest_message, jamais un bouton de réservation. |
requires_approval |
Une réservation ici arrive en pending et le personnel tranche. Ne dites jamais au client qu’il a une table. |
guarantee est non nul quand le créneau demande au client de laisser une carte.
Affichez le montant et les conditions d’annulation avant que le client
s’engage, puis lisez
Ce qu’un tunnel reconstruit ne peut pas faire
avant de concevoir cette étape.
Répondre dans la langue du client
Section intitulée « Répondre dans la langue du client »?locale= fixe la langue des textes rédigés par le restaurant dans la réponse :
name du service, guest_message, day_message et texte de fermeture. Ce
paramètre accepte exactement les langues de guest_languages et toute autre
valeur donne un 400 nommant locale, plutôt qu’un repli silencieux sur la
langue principale.
Ce paramètre concerne le texte que Service vous remet. La langue dans laquelle le
client est écrit est un autre champ, guest.language sur la création, et les
deux se règlent indépendamment.
Sections
Section intitulée « Sections »curl "https://api.useservice.app/v1/sections" \ -H "Authorization: Bearer $SERVICE_API_KEY"Regroupez les lignes par group_id avant d’afficher quoi que ce soit, sinon la
terrasse est proposée deux fois. Quelle ligne nomme un groupe, et quel marqueur
décide de le proposer, sont sur L’objet
section.
show_seating_preference dans la configuration dit si le restaurant souhaite
qu’on pose la question aux clients. Ce champ est indicatif : section_id est
accepté sur les disponibilités et sur une création qu’il vaille true ou
false, et rien n’est refusé parce que vous en envoyez un. C’est le restaurant
qui exprime une préférence sur son propre parcours de réservation et c’est le
seul endroit où cette préférence existe, alors respectez-la plutôt que de
trancher à sa place.
Le même identifiant sec_… part dans ?section_id= sur les disponibilités et
dans section_id sur la création, et n’importe quel membre d’un groupe résout
vers le groupe entier.
Prendre la réservation
Section intitulée « Prendre la réservation »curl -X POST "https://api.useservice.app/v1/reservations" \ -H "Authorization: Bearer $SERVICE_API_KEY" \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -H "Content-Type: application/json" \ -d '{ "service_date": "2026-10-04", "start_time": "20:00", "party_size": 4, "section_id": "sec_7Yd2Qm9", "guest": { "first_name": "Marie", "last_name": "Dupont", "email": "[email protected]", "phone": "+33612345678", "language": "fr" }, "guest_notes": "Table près de la fenêtre si possible", "metadata": { "order_ref": "PKG-40912" } }'Cinq remarques sur cet appel.
Idempotency-Key est obligatoire. Une valeur par tentative de réservation,
la même valeur à chaque nouvel essai de cette tentative. Générez-la avant le
premier appel et conservez-la pour toute la tentative, y compris l’essai qui suit
un refus. Un nouvel essai d’une tentative qui a déjà abouti reçoit la réponse de
cette tentative, marquée Idempotent-Replayed: true. Voir
Idempotence.
Un 201 n’est pas une confirmation. Lisez status. Sous validation
manuelle, la réservation arrive en pending ; sur un créneau portant une
garantie par carte, elle arrive en awaiting_guarantee et ne devient valable
qu’une fois la carte enregistrée par le client. Seul confirmed signifie que la
table est réservée. Votre écran de confirmation a trois issues à afficher, pas
une.
required_guest_fields est la liste réellement appliquée. Elle figure dans
la configuration et une création à laquelle il manque l’un de ces noms est
refusée avec guest_fields_required. Les deux drapeaux voisins,
require_guest_email et require_guest_phone, sont indicatifs : ils
décrivent ce que le restaurant demande sur son propre formulaire et cette API ne
les applique pas. Servez-vous-en pour marquer le même champ obligatoire dans
votre formulaire. Une création sans adresse e-mail ni numéro de téléphone est
acceptée et coûte deux choses au restaurant : personne ne peut joindre ce client
si le service est annulé, et la réservation ne correspond à aucun profil
existant, et elle en crée un nouveau à chaque fois.
Envoyez guest.language. Sans ce champ, chaque client que vous créez est
écrit dans la primary_language du restaurant : confirmation, rappel, lien de
modification, enquête. Silencieusement, puisque rien dans le 201 ne dit quelle
langue est partie. Le champ accepte l’une des onze langues prises en charge par
Service et l’enregistre sur le profil, mais le message part dans la première de
cette langue et de primary_language qui figure dans guest_languages.
Lisez ce tableau pour savoir dans quelles langues le restaurant écrit vraiment.
Que ces messages partent ou non relève du champ guest_notifications, voir
Qui écrit au client.
Un champ que la création ne lit pas est un 400. Un champ du corps mal
orthographié ou non pris en charge est refusé avec
bad_request, param le nommant, au lieu d’être
ignoré. Dans guest, le nom est pointé, comme guest.vip. Les clés de
metadata vous appartiennent et ne sont jamais vérifiées.
Si vous connaissez déjà le client, envoyez guest_id au lieu d’un objet
guest. Une création ne modifie jamais le profil et les coordonnées envoyées à
côté d’un guest_id ne sont enregistrées que sur cette réservation.
Quand la réservation est refusée
Section intitulée « Quand la réservation est refusée »Tout refus dû à une règle de réservation a une seule forme : 422, code de
premier niveau booking_rules_violated et un tableau violations avec une
entrée par règle enfreinte. Le tableau est là pour une règle comme pour six et le
code de premier niveau ne nomme jamais une règle.
{ "error": { "type": "invalid_request_error", "code": "booking_rules_violated", "violations": [ { "code": "slot_no_longer_available", "message": "…", "param": "party_size", "overridable": true } ] }}Un tunnel lit violations et non error.code. Un client qui branche sur le
code de premier niveau fonctionne jusqu’au jour où une réservation enfreint deux
règles à la fois.
Deux entrées méritent leur propre écran plutôt qu’un bandeau générique et elles se confondent facilement :
slot_no_longer_available— un service tourne et son plafond de couverts s’est rempli entre votre lecture des disponibilités et votre création. C’est la course que connaît tout tunnel. Relisez les disponibilités et montrez au client ce qui reste.no_service_at_this_time— aucun service ne couvre cette date et cette heure. Relire les disponibilités et envoyer une heure qu’elles publient est le seul remède ; ce code ne peut pas faire l’objet d’un passage outre.
Refus liés aux règles de réservation
documente chaque code et la signification d’overridable, et
Passer outre une règle couvre le
tableau override et la règle du seul nouvel essai qui l’accompagne. Un tunnel
ouvert au public ne devrait en général pas porter reservations:write:override :
les limites qu’il écarte sont celles du restaurant.
Terminer le travail
Section intitulée « Terminer le travail »Un tunnel qui s’arrête au 201 laisse le client sur votre page de confirmation
sans aucun retour vers sa propre réservation. Deux champs referment cet écart.
Demandez le jeton sur la création :
POST /v1/reservations?expand=modification_tokenmodification_token est la capacité propre du client sur cette réservation.
Ajoutez-le à public_booking_url issu de la configuration et vous obtenez un
lien de gestion fonctionnel — consulter, modifier, annuler — qui ne demande
aucune de vos informations d’authentification :
{public_booking_url}/{modification_token}Trois contraintes l’accompagnent :
- Il n’est renvoyé que sur la réponse de création. Les lectures, les listes et les webhooks ne le portent pas : stockez-le à sa réception.
- Il est conditionné à
guests:read, parce qu’il authentifie l’accès aux coordonnées enregistrées du client. Une clé sans cette portée reçoit un403 insufficient_scopeavecparam: "expand"et ne réserve rien : ajoutez la portée avant d’ajouter le paramètre. - Traitez-le comme le mot de passe du client : il ouvre tout ce que le client peut faire sur cette page, y compris l’étape carte d’une réservation à garantie. Ne le transmettez qu’au client, et ne le journalisez pas ni ne l’affichez en clair. Demander une garantie par carte détaille ce qu’il ouvre.
Lisez public_booking_url dans la configuration plutôt que de coder un hôte en
dur. Cette URL est absolue à dessein, car un chemin relatif se résoudrait contre
l’origine qui sert votre tunnel, et elle change si le slug du restaurant change.
Qui écrit au client
Section intitulée « Qui écrit au client »Par défaut, Service écrit toujours au client. Une réservation prise par un
partenaire est service_managed comme toutes les autres : l’e-mail de
confirmation et le SMS du restaurant partent, et ils portent un lien de
modification/annulation construit à partir de ce même jeton. Vous ne perdez cela
qu’en le demandant : poser guest_notifications: "partner_managed", c’est
affirmer que vous prenez en charge la communication client pour cette
réservation, lien compris. Alors, et alors seulement, perdre le jeton laisse le
client sans autre accès à sa réservation que vous.
guest_notifications se fixe à la création et n’en bouge plus : un PATCH qui
le porte est un 400. Affirmer partner_managed supprime tous les messages
client facultatifs pendant toute la vie de la réservation — confirmation,
modification, annulation, rappel, enquête — sur l’e-mail comme sur le SMS. Les
alertes destinées au personnel du restaurant ne sont pas touchées et vous
continuez de recevoir chaque webhook reservation.*. Ce champ est refusé
d’emblée sur un créneau portant une garantie par carte, avec
guest_notifications_required_for_guarantee :
voir Demander une garantie par carte.
Ce qu’un tunnel reconstruit ne peut pas faire
Section intitulée « Ce qu’un tunnel reconstruit ne peut pas faire »Deux capacités du widget intégré n’existent pas sur cette API et les deux méritent d’être connues avant de promettre un parcours.
L’étape carte ne peut pas être reconstruite. Un créneau portant une
guarantee demande au client d’enregistrer une carte. Votre clé lit le devis
(mode, montant, conditions d’annulation) et jamais les informations de paiement
qui sont derrière, et votre page n’a rien pour monter un formulaire de carte.
La réservation arrive en awaiting_guarantee, Service envoie au client un lien
de paiement par e-mail et l’étape carte se termine sur la page hébergée. Ces
réservations exigent une adresse e-mail : une création sur un créneau
garanti sans adresse est refusée avec
email_required_for_guarantee.
Un client ne peut pas rejoindre la file d’attente depuis ici. Les disponibilités vous disent qu’une file est ouverte pour ce nombre de couverts et les points de terminaison de file d’attente sont en lecture seule. Un client qui veut la file la rejoint depuis le widget, ou le restaurant l’y inscrit depuis le back-office.