Réserver malgré les règles
Certaines réservations sont faites pour enfreindre les règles. Un séminaire de trente sur un service qui plafonne les groupes à huit. Un habitué que le propriétaire installera quoi qu’en dise le compte de couverts. Un groupe vendu par téléphone le mois dernier, sur une soirée dont la réservation en ligne a fermé il y a une heure.
Chacune de ces réservations est un refus sur POST /v1/reservations, et chacune
est une réservation que le restaurant veut. Le tableau override permet de la
prendre : vous nommez la règle que vous écartez, pour cette écriture-là.
D’abord : la liste des codes levables est fixe, et courte
Section intitulée « D’abord : la liste des codes levables est fixe, et courte »Neuf codes peuvent être levés. La liste appartient à l’API, pas aux réglages d’un restaurant : aucun restaurant ne l’étend, et aucune portée n’en débloque davantage.
| Levables | |
|---|---|
slot_no_longer_available |
cutoff_passed |
advance_window_exceeded |
party_size_too_small |
party_size_too_large |
slot_blocked |
shift_not_online_bookable |
duplicate_booking |
guarantee_required |
Tout le reste est un mur. Deux de ces murs sont justement les refus qu’un service grands groupes rencontre le plus :
no_service_at_this_time— aucun service ne couvre cette date et cette heure. Il n’y a aucun service où installer le groupe, aucun budget de couverts à lever et rien qu’unoverridepuisse atteindre. Le seul remède est un autre créneau : relisezGET /v1/availabilityet envoyez une heure qu’il publie. Le restaurant peut ouvrir un service ce soir-là, et la réservation passe alors parce que la grille a changé, pas parce qu’une règle a été écartée.no_table_available— les couverts étaient là, la géométrie non. Aucune table du plan de salle ne place ce groupe à cette heure. Lever ce code produirait une réservation sans table, que le restaurant devrait ensuite afficher sur un plan où rien ne l’accueille. Un séminaire qui dispose d’une salle pour lui seul se réserve avecentire_section_id, qui prend toutes les tables de la section. Voir Réserver une salle entière.
Une boucle de réessai qui renvoie tous les codes qu’on lui a donnés ne se
terminera jamais sur ces deux-là. Lisez overridable sur chaque violation et ne
renvoyez que les codes à true. La liste complète, avec le sens de chaque code,
est dans Refus liés aux règles de
réservation.
Portées nécessaires
Section intitulée « Portées nécessaires »| Portée | Ce qu’elle sert ici |
|---|---|
reservations:write |
La création, la modification, l’option. |
reservations:write:override |
Le tableau override lui-même. Distincte de reservations:write pour qu’une intégration ordinaire ne puisse pas lever une règle par accident. Elle porte aussi excluded_from_shift_limits, pour une privatisation qui ne doit pas peser sur les plafonds de couverts du service — voir Un séminaire demande un seul champ — et entire_section_id. |
reservations:read |
Les disponibilités, pour trouver le créneau. |
Les deux portées d’écriture sont attribuées par le restaurant, sur la clé, dans
son propre back-office. Une clé qui n’en porte pas une ne peut pas se l’accorder
elle-même, et un appel qui demande ce que la clé n’a pas est refusé par un
403 insufficient_scope portant param: "override" — voir
Portées.
Ce param distingue « cette clé ne peut pas appeler ce point de terminaison »
de « cette clé ne peut pas l’appeler ainsi ». Le second cas vaut un réessai
automatisable : retirez le tableau, envoyez la réservation ordinaire, et dites à
la personne au téléphone que la règle tient.
Prendre la réservation, et lire le tableau
Section intitulée « Prendre la réservation, et lire le tableau »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": 30, "guest": { "first_name": "Marie", "last_name": "Dupont", "email": "[email protected]" }, "guest_notes": "Séminaire direction commerciale — salon sud" }'Une réservation qui enfreint des règles revient en 422, avec
booking_rules_violated au niveau supérieur et une entrée dans violations par
règle enfreinte. Le code ombrelle est toujours là, y compris pour une seule
règle, et il ne nomme jamais de règle :
{ "error": { "type": "invalid_request_error", "code": "booking_rules_violated", "violations": [ { "code": "party_size_too_large", "message": "…", "param": "party_size", "overridable": true }, { "code": "cutoff_passed", "message": "…", "overridable": true } ] }}Aucune version de cette réponse ne rapporte une règle à plat. Un client qui
dispatche sur error.code et y trouve party_size_too_large lit une API qui
n’existe pas, et il fonctionnera jusqu’au soir où une réservation enfreindra
deux règles à la fois.
Décider, puis renvoyer une fois
Section intitulée « Décider, puis renvoyer une fois »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": 30, "guest": { "first_name": "Marie", "last_name": "Dupont", "email": "[email protected]" }, "guest_notes": "Séminaire direction commerciale — salon sud", "override": ["party_size_too_large", "cutoff_passed"] }'Cinq points sur ce second appel.
Le tableau est détaillé, et il n’existe aucune forme globale. Pas de
"all", pas de force: true, pas de joker. Un booléen s’active une fois
pendant l’intégration, puis lève en silence toute règle écrite par la suite, y
compris celles que le restaurant ajoutera l’an prochain. Un tableau nomme ce que
vous acceptez pour cette écriture : une règle livrée plus tard reste
appliquée à toutes les intégrations existantes, puisque personne ne l’a nommée.
Le même Idempotency-Key que la tentative refusée. Le refus n’a rien créé
et a rendu la clé : la représenter la réclame à neuf plutôt que de rejouer le
refus. La clé empêche un réessai après délai d’attente de réserver le groupe
deux fois : conservez-la pour cette fenêtre. Voir
Idempotence.
Réessayez une fois, pas en boucle. Relevez les codes levables, décidez, renvoyez. Un second refus est un autre problème de réservation, pas une liste plus longue à renvoyer.
Le second appel peut buter sur un mur que le premier refus n’a pas nommé. La
recherche de table ne tourne qu’une fois chaque règle respectée ou levée :
no_table_available n’est donc jamais listé à côté de party_size_too_large.
Un groupe de trente dont la levée est acceptée a toujours besoin d’une table, ou
d’une combinaison, qui accueille trente personnes. Si le restaurant n’en a pas,
le second envoi revient en 422 avec no_table_available, et la réservation
est impossible à cette heure. Montrez-le à la personne qui a décidé la levée,
au lieu de lui demander de décider à nouveau. Ce qu’un refus ne peut pas vous
dire donne l’ordre des
contrôles.
Le tableau peut être envoyé par anticipation. Un service qui sait qu’il
réserve un séminaire de trente couverts peut envoyer override dès le premier
appel et se passer de l’aller-retour de découverte. Rien n’est consigné quand
rien n’a été levé : un override sur une réservation qui n’enfreint aucune
règle ne vous coûte rien — voir Ce que le restaurant
voit.
Nommer un code non levable donne un 400, dès la requête
Section intitulée « Nommer un code non levable donne un 400, dès la requête »C’est la partie qui boucle la boucle, et elle est voulue. Placez
no_service_at_this_time dans override et toute la requête est refusée par un
400 bad_request avec param: "override", avant le moindre travail de
réservation. Le message distingue les deux erreurs possibles :
`no_service_at_this_time` est une vraie violation mais ne peut jamais être levée.`"slot_at_capacity"` n'est pas un code de violation.
Les deux énumèrent ensuite les neuf codes que le tableau accepte.
Un intégrateur rencontre ce message en écrivant son intégration, et c’est tout
l’objectif. L’autre option — retirer le code non levable et réserver quand
même — rendrait un 201 pour une réservation qui n’a rien levé de ce que
l’appelant croyait avoir levé, ce qui se découvre en salle, à 20:00.
Le même refus couvre la seconde erreur : le vocabulaire d’un document de
conception ou d’un fil de support n’est pas le vocabulaire émis.
slot_at_capacity et past_booking_cutoff ne sont pas des codes de cette API,
et un 400 qui les nomme vaut mieux qu’un silence sans effet.
Où le tableau s’applique aussi
Section intitulée « Où le tableau s’applique aussi »override est accepté sur trois écritures, et chacune est le bon endroit :
| Écriture | Pourquoi |
|---|---|
POST /v1/reservations |
La réservation elle-même. |
PATCH /v1/reservations/{id} |
Agrandir un groupe au-delà du plafond, ou déplacer une réservation après la clôture, enfreint les mêmes règles qu’une création. |
POST /v1/holds |
Les règles sont vérifiées à la pose de l’option, et la conversion ne revérifie délibérément pas la capacité. Une option de trente couverts sur un service où il en reste dix porte son propre override : aucun appel ultérieur ne pourrait le faire. |
Un séminaire demande un seul champ
Section intitulée « Un séminaire demande un seul champ »excluded_from_shift_limits: true indique qu’une réservation ne doit pas peser
sur les plafonds de couverts du service : la privatisation du salon sud qui ne
tire pas sur la cuisine de la salle principale. C’est la raison pour laquelle il
n’existe pas de point de terminaison « bloquer trente couverts » — un blocage
ferme un créneau sans consommer de couverts : une privatisation modélisée en
blocage laisse max_covers intact et le widget continue de vendre des couverts
que personne ne peut cuisiner.
Ce champ n’appartient pas à la famille override, et la distinction survit
à tous les audits ultérieurs de la réservation. Un override dit « j’accepte
d’enfreindre cette règle, maintenant, pour cette réservation ». Celui-ci dit
« cette réservation appartient à un autre pool de capacité ».
Il pèse aussi plus lourd que n’importe quel override. Un override s’épuise
sur une réservation ; celui-ci est permanent et modifie toutes les lectures de
disponibilité de ce service à partir de là. Il avait sa portée propre jusqu’au
22 septembre 2026 et partage désormais reservations:write:override : la
revendication reste distincte, l’autorisation ne l’est plus.
Une clé sans la portée qui envoie true reçoit un 403 insufficient_scope avec
param: "excluded_from_shift_limits". Envoyer false ne demande rien : cela
décrit la réservation que toute clé peut déjà faire. Aucune portée n’est
requise, et une intégration qui envoie le champ à chaque appel n’est jamais
refusée pour avoir dit non.
À la conversion d’une option, le champ voyage avec l’option
Section intitulée « À la conversion d’une option, le champ voyage avec l’option »Cette couture surprend. excluded_from_shift_limits est stocké sur l’option et
n’est pas répété dans le corps de la conversion : une clé qui convertit une
option exonérée affirme l’exonération sans qu’aucun champ de sa requête ne le
dise.
Deux clés rendent la chose concrète. La première porte
reservations:write:override et pose l’option ; la seconde, plus étroite, la
convertit. La conversion est
refusée — 403 insufficient_scope, param: "excluded_from_shift_limits" —
plutôt que de produire une réservation ordinaire qui mangerait en silence tout
le budget de couverts de la soirée.
Ce refus est réversible à dessein. L’option reste posée et convertible, et le
message nomme la sortie : envoyez excluded_from_shift_limits: false et la
conversion aboutit en réservation ordinaire, puisque c’est l’appelant qui
accepte explicitement la classification ordinaire.
Ce que le restaurant voit
Section intitulée « Ce que le restaurant voit »Un override écarte une règle écrite par le restaurant. C’est un acte réel, et
il est consigné comme tel.
L’événement reservation.created de la réservation porte overrides_accepted —
les codes qui ont effectivement levé quelque chose sur cette écriture — et votre
clé peut le relire :
curl "https://api.useservice.app/v1/reservations/resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ/events" \ -H "Authorization: Bearer $SERVICE_API_KEY"{ "object": "reservation_event", "type": "reservation.created", "data": { "source": "api", "party_size": 30, "overrides_accepted": ["party_size_too_large", "cutoff_passed"], "excluded_from_shift_limits": true }}Trois conséquences de la façon dont cette donnée est écrite, à connaître avant de bâtir dessus :
- Appliqué, pas demandé. Envoyer le tableau par anticipation à chaque appel est une forme d’intégration raisonnable, et consigner ce qui a été demandé apposerait une liste identique sur chaque réservation sans rien apprendre à personne. Seul ce qui a réellement levé une règle est consigné.
- Absent, et non vide, quand rien n’a été levé. L’événement
createdd’une réservation ordinaire est identique octet pour octet, qu’elle vienne de votre intégration, du widget ou du back-office. Il n’existe pas d’état « unoverridea été envisagé ». excluded_from_shift_limitsest consigné sur l’événement en plus de la réservation, parce que la colonne seule ne peut pas dire qui l’a posé. Le back-office écrit la même colonne : une réservation exonérée aujourd’hui a pu l’être par votre clé ou cochée par un responsable, et la colonne se lit pareil. « Est-ce que cette intégration s’est mise à tout exonérer ? » est la question que pose un exploitant quand un service se survend, et seule une trace datée au moment de l’affirmation y répond.
Le flux d’événements est décrit dans Événements de
réservation. Un changement du seul champ
émet aussi un webhook
reservation.updated
portant la valeur précédente : un restaurant qui surveille ses propres
intégrations voit la bascule au moment où elle se produit, pas quand le service
affiche complet.
Les refus que ce parcours produit
Section intitulée « Les refus que ce parcours produit »| Code | Où il arrive | Que faire |
|---|---|---|
booking_rules_violated |
422 à la première tentative |
Lisez violations. Jamais le code de niveau supérieur. |
bad_request, param: "override" |
400, avant tout travail de réservation |
Vous avez nommé un code qui ne peut jamais être levé, ou qui n’est pas un code. Corrigez le tableau. |
insufficient_scope, param: "override" |
403 |
La clé ne peut pas lever de règle. Renvoyez sans le tableau, ou demandez au restaurant une clé qui le peut. |
insufficient_scope, param: "excluded_from_shift_limits" |
403 |
À la création, retirez le champ. À la conversion, envoyez false. |
no_service_at_this_time |
dans violations, overridable: false |
Un autre créneau. Rien d’autre ne marche. |
no_table_available |
dans violations, overridable: false, souvent au second envoi seulement |
Une autre heure, un groupe plus petit, ou le restaurant réorganise la salle. Pour un groupe qui occupe une salle entière, réservez la salle. |
guarantee_required |
dans violations, overridable: true |
Le lever réserve sans empreinte de carte. Voir Demander une garantie par carte. |
Erreurs est la référence pour tous ces codes et pour le reste du vocabulaire des violations.
Ce qui sépare ceci d’un drapeau « forcer »
Section intitulée « Ce qui sépare ceci d’un drapeau « forcer » »Une démonstration code override en dur et regarde la réservation passer.
Quatre choses la séparent de ce qu’un restaurant peut supporter au quotidien.
Elle ne lève rien par défaut. Le tableau à chaque appel, rempli avec ce que
disait le dernier refus, c’est un force: true écrit en plus long. Décidez par
réservation, à partir des codes réellement reçus.
Elle demande à un humain là où un humain demanderait.
party_size_too_large sur un groupe de trente est une décision commerciale que
le restaurant a déjà prise : le séminaire est vendu. slot_no_longer_available
sur un groupe de deux est le plafond de couverts qui fait son travail, et le
lever ajoute une table dans une salle que la cuisine a déjà engagée. Les codes
diffèrent pour que vous puissiez les traiter différemment.
Elle tient sa propre trace. overrides_accepted dit au restaurant ce qui a
été levé ; il ne lui dit pas qui a décidé chez vous, ni pourquoi. Mettez la
référence de rapprochement dans metadata et la raison dans guest_notes, que
la salle lit vraiment.
Elle laisse la portée override hors des clés qui n’en ont pas besoin. Un
tunnel de réservation public porte reservations:write et rien de plus. Un
service séminaires, tenu par des personnes que le restaurant connaît, porte la
portée d’override. Une clé par usage est toute la raison d’être de portées
distinctes, et le restaurant peut en révoquer une sans casser l’autre.