Aller au contenu

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.

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.

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.

Fenêtre de terminal
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 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.

Fenêtre de terminal
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.

Fenêtre de terminal
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.

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.

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.

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.