Poser une option
Une option retient un créneau sans le réserver. La table est occupée dès que l’appel répond : une option pèse sur les disponibilités exactement comme une réservation. Aucun client n’y est rattaché.
C’est la forme d’un parcours où la table n’est qu’un article parmi plusieurs. Un package dîner-spectacle vend un billet de concert, une table et une chambre d’hôtel dans un même panier ; le billet est confirmé par un système, la table par celui-ci, et le client paie une seule fois à la fin. Sans option, la table est soit réservée avant que le client ait payé, et il faut l’annuler quand il abandonne le panier, soit réservée après, et le 20:00 que vous aviez annoncé est parti.
Une option n’est pas une réservation
Section intitulée « Une option n’est pas une réservation »Elle n’apparaît pas dans GET /v1/reservations, elle n’a pas de client et
personne ne reçoit de confirmation. Elle vit sur /v1/holds jusqu’à l’une de
quatre issues : vous la convertissez en réservation, vous la libérez, le
restaurant la libère depuis son back-office, ou elle arrive à échéance. La
libération par le restaurant vous parvient comme un hold.released que vous
n’avez pas provoqué, et une conversion après elle donne un 404 — voir les
événements plus bas.
Une option et la réservation qu’elle devient sont la même ligne et leurs
identifiants le disent : hold_AbC se convertit en resv_AbC. L’identifiant
d’option cesse d’apparaître dans GET /v1/holds dès la conversion et continue de
répondre sur GET /v1/holds/{id}, avec status: "converted" et le nom de la
réservation. Rien de ce que vous avez stocké ne devient inatteignable.
Poser une option demande reservations:write ; les lire demande
reservations:read. Il n’existe délibérément pas de portée distincte pour les
options : une clé qui peut réserver peut poser une option. Voir
Portées.
Deux natures, et celle par défaut n’est pas celle qu’il vous faut ici
Section intitulée « Deux natures, et celle par défaut n’est pas celle qu’il vous faut ici »{ "service_date": "2026-10-04", "start_time": "20:00", "party_size": 4, "kind": "commitment" }Envoyez kind explicitement. Omettez-le et vous obtenez basket, une option
de quelques minutes : la bonne pour un tunnel de paiement, la mauvaise pour un package
qui se dénoue le lendemain.
basket |
commitment |
|
|---|---|---|
| Forme du parcours | Un client est en cours de paiement | Une capacité déjà vendue dont vous attendez le règlement |
| Durée maximale accordée | 30 minutes | 48 heures |
| À l’échéance | La table repart à la vente immédiatement | Elle garde la table et attend d’être résolue |
Une option basket se libère d’elle-même à l’instant où elle expire, et un
panier abandonné ne coûte rien au restaurant. Une commitment ne se libère
pas à son échéance : elle garde la table jusqu’à ce qu’un
balayage la résolve, car libérer une capacité que quelqu’un a déjà achetée reviendrait à la
vendre deux fois. Libérez chaque commitment que vous ne convertissez pas.
L’échéance est accordée, pas demandée
Section intitulée « L’échéance est accordée, pas demandée »expires_at est facultatif et c’est le serveur qui tranche. Une échéance
demandée est ramenée dans les bornes plutôt que refusée : demandez 45 minutes
sur un basket et vous en obtenez trente, et la réponse le dit. La durée minimale
des deux natures est de cinq minutes. Ces bornes sont les mêmes pour tous les
restaurants.
Une demande sans expires_at obtient dix minutes pour un basket et la plus
longue option, 48 heures, pour une commitment.
Lisez expires_at dans la réponse, ne supposez jamais votre propre valeur.
Ces bornes sont une décision produit et peuvent bouger. Le ramenage dans les
bornes leur permet de bouger sans casser votre intégration. Une valeur
qui n’est pas un horodatage RFC 3339 donne un 400 nommant expires_at : une
échéance que Service n’a pas su lire n’est pas remplacée en silence par la valeur
par défaut, car vous feriez alors tourner le minuteur de votre panier sur un
nombre que vous n’avez jamais reçu.
L’expires_at demandé ne fait délibérément pas partie de ce que la clé
Idempotency-Key identifie, et un client qui recalcule « maintenant + 10
minutes » à chaque tentative obtient bien sa relecture plutôt qu’un 409.
Poser l’option
Section intitulée « Poser l’option »curl -X POST "https://api.useservice.app/v1/holds" \ -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, "kind": "commitment", "expires_at": "2026-10-02T18:00:00+02:00", "section_id": "sec_7Yd2Qm9", "metadata": { "package_ref": "PKG-40912" } }'Idempotency-Key est obligatoire, pour une raison qui lui est propre en plus de
la raison habituelle : une option en double prend silencieusement une deuxième
table, et l’identifiant que vous avez perdu est le seul moyen de la nommer. Vous
ne pouvez pas libérer ce que vous n’avez jamais reçu. Réessayez avec la même clé
et vous recevez la première option, identifiant compris, avec l’en-tête
Idempotent-Replayed: true.
Les règles sont vérifiées ici, pas à la conversion. Capacité, heure de
fermeture, blocages et section sont tous évalués au moment de la pose. Une
option vous achète ces vérifications ; la conversion ne les répète pas. Un refus
arrive donc sur cet appel, sous la même forme qu’une création : 422 booking_rules_violated avec un tableau violations. Lisez le tableau et non le
code de premier niveau, voir
Refus liés aux règles de réservation.
override se place sur cet appel aussi, pour la même raison.
L’horaire d’arrivée fait partie de ces contrôles. Le start_time d’une option
doit être une heure que GET /v1/availability propose, et 20:07 dans un
restaurant qui accueille toutes les demi-heures est refusé avec
start_time_off_grid.
La convertir
Section intitulée « La convertir »La conversion est une création qui nomme l’option. Elle vit sur
POST /v1/reservations et non sur la collection des options, parce qu’elle
produit une réservation et parcourt tout le chemin de réservation : résolution du
client, branche garantie, e-mail de confirmation.
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 '{ "hold_id": "hold_AbC3xK9", "guest": { "first_name": "Marie", "last_name": "Dupont", "email": "[email protected]" } }'Le créneau vient de l’option. Omettez service_date, start_time, party_size
et section_id, ou répétez-les à l’identique : une valeur qui diverge donne un
400, car une option
ne se déplace pas.
La conversion est une écriture distincte et exige sa propre Idempotency-Key,
pas celle qui a servi à poser l’option.
Tout ce qui concerne le client s’applique encore. Une conversion sur un créneau
portant une garantie par carte arrive en awaiting_guarantee exactement comme
une réservation nouvelle : lisez status dans la réponse plutôt que de
supposer que la conversion a confirmé quoi que ce soit.
Les metadata posées sur l’option sont reportées sur la réservation, sauf si la
conversion en envoie de nouvelles.
Le champ excluded_from_shift_limits de l’option est lui aussi reporté sur la
réservation. Il indique si l’option compte dans les plafonds de
couverts du service, et convertir une option où il vaut true exige
reservations:write:override. Une clé sans cette portée est refusée
avec 403 insufficient_scope et param: "excluded_from_shift_limits", et peut
envoyer excluded_from_shift_limits: false pour convertir l’option en
réservation ordinaire. Lisez ce champ sur l’option avant de convertir une option
posée par une autre intégration du restaurant.
L’option est consommée de façon atomique, et un hold_id inconnu, déjà
converti, libéré ou expiré donne un 404. Le remède est de réserver le créneau
à neuf sans lui, ce qui rejoue tous les contrôles de capacité que l’option avait
achetés.
Libérer ce que vous ne convertissez pas
Section intitulée « Libérer ce que vous ne convertissez pas »curl -X DELETE "https://api.useservice.app/v1/holds/hold_AbC3xK9" \ -H "Authorization: Bearer $SERVICE_API_KEY"C’est l’appel qu’une intégration bien élevée fait sur chaque panier abandonné, et
il n’est pas facultatif pour une option commitment, qui ne se libère pas
d’elle-même.
DELETE est idempotent : libérer une option déjà libérée ou déjà expirée renvoie
le même objet terminal avec un 200, et un nouvel essai qui avait en fait
réussi ne demande aucun traitement particulier. Une option devenue réservation est
le seul refus, un 409
hold_already_converted qui nomme la
réservation. Annulez cette réservation à la place.
Changer l’heure
Section intitulée « Changer l’heure »Une option garde le créneau sur lequel elle a été posée. Quand le client veut une autre heure, posez une nouvelle option et libérez celle-ci — voir Changer l’heure d’une option.
Quatre événements, et un seul signifie qu’une réservation existe
Section intitulée « Quatre événements, et un seul signifie qu’une réservation existe »| Événement | Ce qui s’est passé |
|---|---|
hold.created |
Un créneau a été retenu. La table est occupée et personne n’a réservé. |
hold.converted |
L’option est devenue la réservation qu’elle nomme. |
hold.released |
L’option a été rendue avant son échéance, par vous ou par le restaurant depuis son back-office. |
hold.expired |
L’option est arrivée à échéance. |
hold.converted est le seul signal que l’option est devenue une réservation. Il
part en même temps que reservation.created et les deux décrivent le même
groupe : comptez la conversion comme la libération de l’option, sinon vous
compterez les couverts deux fois. Le
catalogue d’événements porte chaque charge
utile.
Deux choses qui surprennent
Section intitulée « Deux choses qui surprennent »Les options sont rattachées au restaurant, pas à votre clé.
GET /v1/holds renvoie toutes les options du restaurant, quelle que soit
l’intégration qui les a posées, y compris les metadata de chacune. C’est la
même frontière que pour les réservations, une intégration qui voit les
réservations du restaurant voit ce qui retient ses tables, et c’est délibéré. La
conséquence pour vous porte sur ce que vous écrivez : mettez des identifiants de
rapprochement dans metadata, pas des conditions commerciales ni quoi que ce
soit que vous ne montreriez pas à une autre intégration du restaurant.
Le nombre d’options ouvertes n’est pas plafonné. La limite de débit en
écriture borne la vitesse à laquelle vous pouvez demander, pas le volume que vous
pouvez retenir, et une option commitment ne se libère pas à la lecture. Vos
options consomment les disponibilités du restaurant : une série d’options jamais
résolues se lit côté restaurant comme un service complet que personne n’a
réservé. Libérez à l’abandon, posez l’échéance la plus courte que votre parcours
peut tenir et rapprochez sur hold.expired.
Relire les options
Section intitulée « Relire les options »GET /v1/holds liste les options vivantes, libérées et expirées, la plus récente
en tête, avec les filtres status et date ainsi que la
pagination par curseur habituelle. Les options
converties sont absentes de la liste à dessein : ce sont des réservations
désormais. Demander ?status=converted donne un 400.
GET /v1/holds/{id} résout n’importe quelle option, y compris convertie, qui
répond alors status: "converted" et nomme la réservation dans reservation.
C’est l’appel qui retransforme après coup un identifiant d’option stocké en
quelque chose d’utile.