Garder votre système synchronisé
C’est vous qui détenez la fiche client. Le CRM d’un groupe hôtelier, l’entrepôt de reporting d’une agence, le système adhérents d’un programme de fidélité — quelque chose qui sait déjà qui sont ces personnes et doit maintenant savoir ce qu’elles ont réservé. Les réservations du restaurant doivent y arriver et y rester à jour, y compris celles que personne chez vous n’a prises.
Trois mécanismes portent cela, et une synchronisation qui tient un CRM les utilise tous les trois.
D’abord : les webhooks seuls ne sont pas un registre
Section intitulée « D’abord : les webhooks seuls ne sont pas un registre »Un webhook est le signal qu’il s’est passé quelque chose. Ce n’est pas la garantie que vous avez tout appris, et quatre propriétés de cette distribution le rendent concret :
- La distribution est au moins une fois, et sans ordre. Vous pouvez recevoir
reservation.seatedavantreservation.confirmed, et recevoir l’un ou l’autre deux fois. - Un point de réception en échec pendant environ trois jours est désactivé, et les événements émis pendant son indisponibilité ne sont pas mis en file pour vous.
- Les réservations importées depuis un autre prestataire n’émettent rien. Un restaurant en migration continue d’importer, et rien de ce trafic ne déclenche de webhook.
- Une réservation sur un créneau à garantie est publiée tard, et parfois jamais. Voir Les réservations à garantie arrivent dans le désordre.
Les webhooks vous disent quand regarder.
updated_sincevous dit ce qui est vrai.
Construisez les deux. Le gestionnaire de webhooks garde votre CRM vivant ; un
balayage updated_since périodique le remet juste après une panne, une
migration ou une distribution manquée. Une intégration réduite au premier
est juste la plupart du temps et discrètement fausse le jour qui compte.
Portées nécessaires
Section intitulée « Portées nécessaires »| Portée | Ce qu’elle sert ici |
|---|---|
reservations:read |
Les réservations, leurs événements, les options et la file d’attente. Toute la surface de réservation. |
guests:read |
Les fiches client : GET /v1/guests, ?expand=guest et ?expand=modification_token sur POST /v1/reservations. Aussi les coordonnées et les notes client de chaque réservation, et les notes de chaque entrée de file d’attente. |
Une réservation porte sa propre taille de groupe, ses horaires et son statut ;
la personne est un identifiant gst_… et rien de plus tant que vous ne
l’étendez pas. Les coordonnées propres à la réservation, les notes du client et
la note du restaurant (contact_email, contact_phone, guest_notes,
internal_notes) sont aussi des données client, absentes pour une clé sans
guests:read : voir
Données client d’une réservation.
Une clé limitée à reservations:read synchronise un flux de réservations complet
et aucune coordonnée.
C’est la bonne clé pour un entrepôt de reporting et la mauvaise pour un CRM.
Décidez ce que vous construisez, et demandez au restaurant la portée la plus
étroite quand elle suffit : les noms, adresses et numéros des clients sont les
relations du restaurant, et une portée que vous ne détenez pas est une portée
que vous ne pouvez pas laisser fuir. expand=guest est contrôlé en un point de
passage unique : une clé sans guests:read reçoit un 403 insufficient_scope
avec param: "expand" d’où qu’elle atteigne le client, y compris depuis une
entrée de file d’attente. Voir Portées.
Enregistrer le point de réception, et vérifier ce qui arrive
Section intitulée « Enregistrer le point de réception, et vérifier ce qui arrive »Les points de réception sont en libre-service : le restaurant ajoute le vôtre dans Paramètres → Développeurs, choisit les événements et révèle un secret de signature. Jusqu’à cinq points par restaurant, chacun épinglé à la version de l’API en vigueur à sa création.
Deux pages couvrent le reste, et ce sont elles la référence :
- Webhooks — l’enveloppe, les réessais, la
désactivation automatique, et pourquoi vous dédupliquez sur
event.id. - Vérifier les signatures — à faire avant d’analyser le corps. L’URL de votre point de réception est publique ; la signature est la seule chose qui atteste que la livraison vient de Service.
Le catalogue d’événements liste les 30 types d’événements avec une charge utile complète pour chacun.
Les portées de votre clé n’atteignent pas le point de terminaison. Une livraison
porte les coordonnées et les notes de la réservation, et les événements
guest.* portent la fiche client, même quand la clé avec laquelle vous
synchronisez ne porte que reservations:read. Voir
Les livraisons portent les données client.
Savoir quels événements se déclenchent, et lesquels non
Section intitulée « Savoir quels événements se déclenchent, et lesquels non »Le catalogue répond événement par événement. Quatre schémas causent la plupart des bugs de synchronisation, et il vaut la peine de les avoir en tête en écrivant le gestionnaire.
Un statut atteint automatiquement ne déclenche rien de plus.
reservation.confirmed se déclenche quand une réservation en attente est
approuvée plus tard. Une réservation confirmée d’office à la création ne le
déclenche jamais : son reservation.created portait déjà
status: "confirmed". Un gestionnaire qui attend confirmed avant d’écrire la
réservation dans votre CRM attendra indéfiniment chez la plupart des
restaurants.
Certaines transitions déclenchent deux événements. Une option convertie émet
hold.converted et reservation.created, à propos du même groupe. Une
promotion depuis la file d’attente émet waitlist_entry.promoted et un
événement de réservation. Comptez un événement par paire, sinon vous comptez les
couverts deux fois.
Un changement du seul champ d’exonération émet désormais
reservation.updated. Poser ou retirer excluded_from_shift_limits ne
changeait rien de visible auparavant ; l’événement part maintenant, avec
previous_attributes portant l’ancienne valeur. C’est le signal qu’une
réservation a quitté ou rejoint le pool de couverts du service, ce qui compte
pour tout ce qui, chez vous, rapporte sur la capacité. Voir Réserver malgré les
règles.
previous_attributes ne voyage que sur les événements *.updated, et parmi
ceux-là seuls reservation.updated et waitlist_entry.updated en construisent
un — guest.updated n’en porte aucun. Sur tous les autres types, data.object
est la ressource complète et le différentiel est à vous de le calculer contre
votre propre copie. La règle propre au champ est sur L’objet événement de
webhook.
Les réservations à garantie arrivent dans le désordre
Section intitulée « Les réservations à garantie arrivent dans le désordre »Cette section mérite d’exister, parce qu’une synchronisation fondée sur les seuls webhooks se trompe dans les deux sens.
Une création sur un créneau portant une garantie par carte répond 201 avec la
réservation en awaiting_guarantee, et le client reçoit un lien de paiement par
e-mail. La réservation est lisible par votre clé dès cet instant :
GET /v1/reservations/{id} la résout et ?status=awaiting_guarantee la
retourne. Mais son webhook reservation.created est retenu jusqu’à
l’enregistrement effectif de la carte.
Deux conséquences :
- Le webhook arrive après la réservation. Si le client enregistre sa carte
deux heures plus tard, c’est à ce moment-là que
reservation.createdpart, bien après votre201, avecstatusàconfirmed, ou àpendingquand le service demande une validation. UnPATCHqui supprime le besoin de carte le fait partir aussi. Aucun autre événementreservation.*sur la réservation ne le précède, pas même lereservation.updatedqu’unPATCHentre-temps aurait envoyé. - Si le client n’enregistre jamais de carte, rien ne part. La demande expire,
la réservation est annulée, et il n’y a pas non plus de
reservation.cancelled: un webhook d’annulation sanscreatedavant lui décrirait une réservation dont votre système n’a jamais entendu parler.
Une synchronisation qui se recale sur l’API plutôt que sur le flux d’événements voit la réservation, la voit se régler et la voit disparaître. Une synchronisation qui se fie au flux n’en voit rien. Demander une garantie par carte couvre la suite de ce parcours.
Se recaler avec updated_since
Section intitulée « Se recaler avec updated_since »curl -G "https://api.useservice.app/v1/reservations" \ -H "Authorization: Bearer $SERVICE_API_KEY" \ --data-urlencode "updated_since=2026-09-21T06:00:00Z" \ --data-urlencode "limit=100"Le flux de changements est disponible sur GET /v1/reservations,
GET /v1/guests et GET /v1/waitlist-entries. Le passer bascule le tri en
(updated_at, id) croissant : vous lisez le changement le plus ancien d’abord
et paginez avec starting_after comme ailleurs.
Pagination en donne la mécanique, y compris
pourquoi le flux est « au moins une fois » et pourquoi votre fenêtre doit
chevaucher la précédente au lieu de reprendre exactement où vous vous êtes
arrêté.
Trois choses que ce flux ne fait pas :
- Les options n’ont pas d’
updated_since.GET /v1/holdsfiltre surstatusetdateà la place. Une option est brève par construction : les quatre événementshold.*sont sa surface de synchronisation, et une option convertie laisse une réservation que le flux porte, lui. - Les statistiques client sont calculées à la lecture.
visit_count,no_show_countetcancellation_countne font pas partie de la surface synchronisée et ne déplacent pas à eux seuls l’updated_atd’un client. Lisez la fiche quand vous en avez besoin. - Il ne dit pas ce qui a changé, seulement que quelque chose a changé. Comparez à votre propre copie, ou lisez les événements de la réservation pour un historique champ par champ.
Choisissez une cadence selon le besoin du restaurant, pas selon ce que la limite autorise. Une fois par minute est généreux pour un balayage de recalage placé à côté d’un gestionnaire de webhooks vivant ; une fois par heure suffit à un entrepôt.
Lire les réservations d’un soir
Section intitulée « Lire les réservations d’un soir »updated_since répond à ce qui a changé. Pour lire ce qui est réservé ce soir,
filtrez plutôt sur la date de service :
curl -G "https://api.useservice.app/v1/reservations" \ -H "Authorization: Bearer $SERVICE_API_KEY" \ --data-urlencode "date=2026-09-21" \ --data-urlencode "limit=100"date[gte] et date[lte] prennent un intervalle, bornes incluses, et
GET /v1/holds accepte les trois mêmes. Sans updated_since, la liste est triée
par date puis par identifiant, la plus récente en premier. Le filtre s’appelle
date bien que le champ de chaque réservation soit service_date :
?service_date= est refusé par un 400 qui le nomme, si bien qu’une faute de
frappe ne renvoie jamais toutes les dates sous l’apparence d’une réponse
filtrée.
Payer dans le bon budget
Section intitulée « Payer dans le bon budget »Les lectures et les écritures tirent sur deux budgets distincts, choisis par
méthode HTTP : GET, HEAD et OPTIONS dépensent le budget de lecture, toute
autre méthode celui d’écriture. Une synchronisation est presque entièrement
faite de GET : elle dépense du quota de lecture et laisse intact le budget
d’écriture, de loin le plus petit des deux, pour le reste de votre intégration.
Cela compte plus qu’il n’y paraît. Une boucle d’interrogation qui reste dans le
budget de lecture ne peut pas affamer une création, et une rafale de créations
ne peut pas bloquer la synchronisation. Lorsqu’une
réponse porte RateLimit-Resource, lisez-le pour savoir lequel des deux budgets
les autres en-têtes décrivent ; les
chiffres, le comportement en rafale et la forme du 429 sont dans Limites de
débit.
Rendre la revalidation bon marché
Section intitulée « Rendre la revalidation bon marché »Les GET de ressource unique portent un ETag. Renvoyez-le en If-None-Match
et une ressource inchangée répond 304 Not Modified sans corps :
curl -i "https://api.useservice.app/v1/reservations/resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ?expand=guest" \ -H "Authorization: Bearer $SERVICE_API_KEY" \ -H 'If-None-Match: W/"resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ-1719515575000000-guest-guest_data"'L’ETag varie selon l’ensemble expand : si vous mettez en cache par
identifiant de ressource, il faut désormais mettre en cache par
(identifiant, expand). La raison est un bug qu’il vaut mieux comprendre que
contourner. Un ETag ignorant expand permettait à un client détenant le corps
simple de revalider en demandant ?expand=guest et de recevoir un 304. Or un
304 signifie « le corps que vous détenez est à jour » : le client en
concluait que l’objet client n’existe pas — une mauvaise réponse qui ressemble
exactement à une bonne.
L’ensemble est trié avant d’entrer dans la clé : expand=guest,events et
expand=events,guest partagent un validateur.
L’ETag varie aussi selon les données client. Un corps qui porte les
coordonnées et les notes de la réservation se termine par -guest_data : un
client qui passe à une clé portant guests:read reçoit un 200 avec le corps
complet, jamais un 304 pour la copie qu’il détient sans elles. Une requête
sans expand, pour un corps sans données client, produit l’ETag que cette API a
toujours produit.
Étendre les objets décrit ce que chaque ressource peut étendre et la taille de page réduite qui accompagne une extension sur une liste.
Les refus que ce parcours produit
Section intitulée « Les refus que ce parcours produit »| Code | Quand | Que faire |
|---|---|---|
insufficient_scope, param: "expand" |
403 sur toute lecture atteignant un client |
La clé n’a pas guests:read. Retirez le paramètre et synchronisez les réservations sans coordonnées, ou demandez au restaurant une clé qui la porte. |
insufficient_scope, sans param |
403 |
La clé ne peut pas appeler ce point de terminaison. Renvoyer ne change rien. |
bad_request, param: "updated_since" |
400 |
L’horodatage n’est pas au format ISO-8601. |
bad_request, param: "service_date" |
400 |
Le filtre de liste est date, ou date[gte] et date[lte]. |
bad_request, param: "expand" |
400 |
Une relation inconnue. Une faute de frappe ne livre jamais en silence des données non étendues. |
rate_limited |
429 |
Respectez Retry-After, puis reculez avec un peu de gigue. Regardez RateLimit-Resource pour savoir quel budget est épuisé. |
not_found |
404 |
Une ressource appartenant à un autre restaurant répond 404, pas 403. Une clé, un restaurant. |
plan_required |
403 |
L’accès à l’API est une option Premium. La clé est valide ; l’abonnement du restaurant ne l’est pas. |
Ce qu’une synchronisation de production fait de plus
Section intitulée « Ce qu’une synchronisation de production fait de plus »Une démonstration affiche le corps du webhook. Cinq choses la séparent d’une synchronisation qu’un groupe de restaurants peut exploiter.
Elle vérifie chaque signature, y compris celles qu’elle s’apprête à ignorer. L’URL est publique. Un corps non signé est l’opinion d’un inconnu sur votre CRM.
Elle est idempotente des deux côtés. Dédupliquez les webhooks sur
event.id, insérez-ou-mettez-à-jour le flux updated_since par identifiant de
ressource, et les deux mécanismes cessent de se disputer la même réservation.
Vous pouvez alors lancer le balayage aussi souvent que vous voulez.
Elle stocke le repère qu’elle a réellement terminé. Pas « maintenant », et
pas le dernier updated_at lu : la dernière page validée. Reprenez un peu avant
ce repère, et acceptez les doublons.
Elle se recale à intervalle régulier, pas seulement après un incident. Un balayage horaire retrouve une réservation importée, une demande de garantie expirée en silence et une livraison perdue, sans que personne ait remarqué leur absence. Un balayage lancé quand quelqu’un se plaint les retrouve une semaine trop tard.
Elle détient les portées qu’elle utilise, et pas davantage. Un entrepôt qui
rapporte sur les couverts n’a pas besoin de guests:read. La demander quand
même fait du fichier client du restaurant votre problème à protéger, sans aucune
fonctionnalité en échange.