Fonctionnement de la livraison

Le contenu standard, le délai de 5 secondes, les raisons de l’absence de nouvelles tentatives et la signification de chaque statut de livraison.

L'envoi des webhooks est volontairement simple, et cette simplicité a des conséquences pour votre récepteur : une seule tentative HTTP, cinq secondes, aucune nouvelle tentative. Lisez cette page avant de décider de la quantité de traitement que votre gestionnaire effectuera de manière synchrone.

La structure standard des données

Tous les événements sauf listing.submission arrivent avec la même structure de premier niveau, les champs propres à chaque événement étant imbriqués sous data.

JSON
{  "event": "connection.location_connected",  "timestamp": "2026-08-25T14:32:10Z",  "account_id": 11073,  "location_id": "279381",  "data": {    "platform": "google",    "connected_account_id": "ba03bc4b-9f8c-4d3b-8e93-9628034c63cc"  }}
ChampTypeRemarques
eventstringLe nom de l'événement, dont les parties sont séparées par des points. Utilisez-le pour déterminer le traitement à effectuer.
timestampstringHorodatage UTC au format ISO-8601, défini lors de la préparation de l'envoi, et non au moment de la modification sous-jacente.
account_idintegerLe compte auquel appartient l'événement.
location_idstringUne chaîne numérique : "279381", et non 279381. Aucune conversion de type pour interaction.* ; voir l'exception ci-dessous.
dataobjectChamps propres à l'événement. Documentés pour chaque événement dans la référence des événements.
agency_account_idintegerPrésent uniquement lorsque le compte est géré par un compte d'agence parent.

Les champs facultatifs de data sont omis lorsqu'ils sont vides, au lieu d'être envoyés avec la valeur null. Prévoyez ces cas dans votre code : traitez de la même manière une clé absente et une valeur null explicite, et ne supposez jamais qu'une clé documentée comme facultative sera présente.

Deux exceptions

Le délai de 5 secondes

Chaque envoi consiste en un seul POST avec un délai de connexion de 5 secondes et un délai de lecture de 5 secondes. Si votre point de terminaison n'a pas répondu à temps, la tentative est abandonnée et enregistrée comme timeout.

Ce délai couvre l'ensemble du traitement effectué par votre gestionnaire. La seule conception sûre consiste à :

  1. Vérifier la signature.
  2. Enregistrer les données dans un stockage durable : une file d'attente, une table, un journal.
  3. Renvoyer 200.

Tous les autres traitements (enrichissement, distribution à plusieurs destinataires, appels à l'API REST, toute interaction avec un service tiers) doivent être effectués après la mise en file d'attente.

Aucune nouvelle tentative n'est effectuée

Chaque envoi fait l'objet d'une seule et unique tentative. Il n'y a ni temporisation progressive, ni file d'attente pour les nouvelles tentatives, ni file d'attente de lettres mortes. Si votre point de terminaison est indisponible pendant deux minutes, les événements déclenchés durant ces deux minutes sont perdus. Ils n'arriveront pas plus tard.

Deux conséquences doivent être explicitées :

  • Les redirections ne sont pas suivies. Les redirections sont entièrement désactivées : si votre point de terminaison renvoie 301 ou 302, l'envoi est enregistré comme un échec et aucune seconde URL n'est appelée. Configurez l'URL de vos webhooks avec l'URL finale. Faites attention aux redirections du domaine nu vers www et à celles qui ajoutent une barre oblique finale, que de nombreux serveurs web appliquent par défaut.
  • Seul un 2xx est considéré comme un envoi réussi. 3xx, 4xx et 5xx sont tous enregistrés comme des échecs.

Doublons et ordre

  • Vous pouvez recevoir deux fois le même événement, et le même id pour deux modifications différentes. Les identifiants dans data (un id d’interaction, un id d’élément de publication, un id de compte connecté) désignent la ressource, pas la modification. interaction.review se déclenche à nouveau avec le même id d’interaction chaque fois que l’avis change, et aucune charge utile ne contient de version ni de champ updated-at : un gestionnaire qui ignore définitivement les événements sur la base de l’id + event écarte donc silencieusement de véritables modifications. Détectez plutôt les doublons exacts : calculez le hachage des octets bruts du corps et ignorez toute répétition du même hachage dans une courte fenêtre de durée limitée (quelques minutes, pas indéfiniment). Un corps différent pour le même id correspond à une nouvelle modification, pas à un doublon. Relisez ensuite la ressource via l’API REST et considérez le résultat de cette lecture comme la référence ; utilisez l’id de la ressource comme clé pour l’écriture au lieu d’omettre celle-ci.
  • L’ordre n’est pas garanti, et timestamp n’y changera rien. Les événements proviennent de plusieurs flux indépendants et sont transmis à mesure qu’ils arrivent ; un local_post.published peut donc arriver avant le local_post.created que vous attendiez en premier. N’ordonnez pas deux modifications de la même ressource selon timestamp : cet horodatage est défini lors de la préparation de l’envoi, pas au moment de la modification, et la charge utile ne contient aucune version ni aucun numéro de séquence. Lorsque l’ordre compte, relisez la ressource et utilisez ce que l’API renvoie.
  • profile.updated est limité à un envoi par établissement toutes les 60 secondes, au début de la fenêtre. Il s’agit bien d’une suppression des événements suivants, pas d’un délai : la première modification ouvre la fenêtre et est transmise, toutes les modifications effectuées dans cette fenêtre sont écartées et n’arrivent jamais, et changed_fields décrit uniquement la modification qui a ouvert la fenêtre. L’événement vous parvient donc au début de la fenêtre ; une relecture déclenchée par son arrivée ne peut donc voir que cette première modification. Planifiez une seconde relecture après la fermeture de la fenêtre, au moins 60 secondes après l’événement, et synchronisez également les données des établissements à intervalles réguliers, car la notification elle-même peut se perdre : une seule tentative, sans nouvelle tentative. Aucun autre événement n’est soumis à ce mécanisme de limitation.

Récupérer les données après un événement manqué

Il n’y a ni nouvelle tentative ni retransmission ; chaque famille nécessite donc une lecture de référence à laquelle vous pouvez recourir. Synchronisez les données à intervalles réguliers, pas seulement lorsqu’un problème semble se produire.

FamilleRelire avecIntervalle suggéréCe qu’une lecture ne permet pas de récupérer
listing.submissionGET /locations/{locationId}/listings/premium, une fois avec listingType=premium et une fois avec additional : les valeurs syncStatus et listingUrl par site annoncées par l’événement.Chaque jour pour chaque établissement, et après une modification groupée d’établissements.L’ancienne charge utile ne contient ni timestamp ni account_id ; un événement manqué ne peut donc être ni situé dans le temps ni attribué à un compte. Aucune lecture n’expose error_message pour une soumission incomplete.
profile.*GET /locations, GET /locations/search ou GET /locations-by-ids. La valeur updatedAt de chaque établissement vous indique lesquels ont changé.Toutes les heures, ainsi que la relecture après la fermeture de la fenêtre décrite ci-dessus.source (user / api / bulk_edit) et changed_fields : une lecture indique l’état actuel, jamais qui a modifié quoi. Les modifications écartées par le mécanisme de limitation de 60 secondes ne laissent aucune trace, et l’état d’un établissement avant sa suppression est perdu.
connection.*GET /connected-accounts et GET /connected-accounts/{connectedAccountId}/details pour la connexion, la déconnexion et l’état de l’authentification. Pour la paire d’événements de vérification Google, récupérez googleVerificationStatus { status, message } dans le résultat de n’importe quelle lecture d’un établissement.Toutes les heures ; toutes les quelques minutes pendant une vérification.failure_type (rejected ou transient) et la chaîne reason. Les interruptions temporaires sont invisibles : une connexion interrompue puis rétablie apparaît comme connectée à la lecture.
interaction.*GET /locations/{locationId}/reviews par établissement, GET /rollup_interactions sur plusieurs établissements, GET /reviewDetails par id.Toutes les 15 minutes pour les nouveaux avis, chaque jour pour une vérification plus large.Le fait même qu’une modification a eu lieu. Les champs d’interaction documentés ne contiennent pas de champ updated-at ; un avis modifié se présente donc exactement comme un avis que vous avez déjà enregistré : comparez les champs que vous conservez.
local_post.*GET /locations/{locationId}/posts, GET /locations/{locationId}/bulk-posts, GET /posts/{postId} pour le statut actuel de publication par site.Toutes les heures pendant le traitement des publications.rejection_reason tel qu’il a été transmis, et la succession des états par lesquels un élément de publication est passé. Les lectures fournissent le statut actuel, pas les transitions.
review_analytics.*GET /locations/{locationId}/review-analytics-overview avec les valeurs startDate et endDate de la période.Chaque jour après la fenêtre d’agrégation, chaque semaine pour la famille hebdomadaire.Les chiffres à la date de l’instantané. La vue d’ensemble est recalculée au moment de la lecture ; un avis arrivé tardivement ou supprimé entraîne donc un écart par rapport au contenu de l’instantané.

Résultats des livraisons

Chaque tentative, même lorsqu’aucune requête n’est envoyée, est enregistrée avec un statut. Voici les chaînes exactes.

Les quatre résultats et les chaînes de statut qui les consignent. « Bloqué » et « Limite de débit atteinte » signifient qu’aucune requête HTTP n’a été effectuée, ce qui justifie de les distinguer d’un échec réel.
StatutRésultatSignificationAction à effectuer
successLivréVotre point de terminaison a renvoyé un code 2xx.Aucune.
un code HTTP sous forme de chaîne, e.g. "404", "500"ÉchecVotre point de terminaison a renvoyé ce code hors de la plage 2xx. 3xx entre également dans cette catégorie, car les redirections ne sont pas suivies.Vérifiez le chemin, la chaîne de redirections et le taux d’erreur de votre gestionnaire.
999ÉchecUne réponse a été reçue sans code de statut exploitable.La cause est généralement un proxy ou un répartiteur de charge placé devant votre point de terminaison.
timeoutÉchecAucune réponse dans le délai de 5 secondes.Déplacez le traitement en dehors du cycle de la requête et accusez réception plus rapidement.
network_errorÉchecLe nom d’hôte n’a pas pu être résolu ou l’hôte n’a pas pu être joint.Vérifiez le DNS et assurez-vous que le nom d’hôte peut être résolu publiquement.
errorÉchecLa tentative a déclenché une erreur qui ne figure pas dans les cas ci-dessus, comme un échec de négociation TLS.Vérifiez votre chaîne de certificats et les journaux du serveur.
blocked_urlBloquéL’URL a échoué au contrôle de sécurité au moment de la connexion : elle n’utilise pas HTTPS, ne comporte pas d’hôte ou a été résolue en une adresse privée, de bouclage, de liaison locale ou de métadonnées.Faites pointer l’URL vers un hôte HTTPS public. Ce contrôle est effectué à chaque livraison : une modification DNS peut donc entraîner le blocage d’une URL qui fonctionnait auparavant.
endpoint_unverifiedBloquéLe compte dispose d’un secret de signature, mais le point de terminaison n’a pas été validé ou sa vérification a été réinitialisée.Relancez Vérifier le point de terminaison depuis le tableau de bord.
rate_limitedLimite de débit atteinteLe plafond par minute ou par jour a été dépassé. L’événement a été supprimé.Réduisez le volume ou passez à un forfait avec un plafond plus élevé. Consultez les forfaits et les limites.

Liste de vérification pour le récepteur

  • Vérifiez la signature de chaque requête ; renvoyez 401 et n’effectuez aucun traitement si elle ne correspond pas.
  • Calculez le hachage des octets bruts du corps, jamais celui d’un objet sérialisé à nouveau.
  • Renvoyez 2xx bien avant l’expiration du délai de 5 secondes ; mettez le traitement en file d’attente.
  • Utilisez les identifiants présents dans data comme clés pour chaque écriture effectuée par le gestionnaire afin qu’une répétition produise le même résultat, mais ne considérez jamais qu’un identifiant déjà reçu signifie qu’il n’y a rien à faire.
  • Ne déduisez pas l’ordre des événements de leur ordre d’arrivée et ne les triez pas selon timestamp.
  • Normalisez location_id en chaîne de caractères dès sa réception.
  • Ignorez les valeurs de event que vous ne reconnaissez pas et renvoyez tout de même 200.
  • Servez directement l’URL des webhooks, sans redirection.
  • Vérifiez et corrigez périodiquement la cohérence des données avec l’API REST ; aucune nouvelle tentative n’est prévue pour rattraper les échecs.