Webhooks et vérification de signature

Recevoir sur votre serveur chaque nouvel avis, retour privé ou client, vérifier la signature Standard Webhooks, puis gérer relances, désactivation et rejeu.

Documentation de l'APIGuides · Webhooks

Principe

Un webhook envoie à votre serveur, en POST HTTPS, chaque événement du compte auquel vous vous êtes abonné : un nouvel avis, une réponse publiée, un retour privé, un nouveau client. Votre outil n'a plus besoin d'interroger l'API pour savoir ce qui a changé.

  • Un point de réception reçoit les événements de tous les établissements du compte, à partir de sa création.
  • Chaque livraison est signée selon la spécification ouverte Standard Webhooks, dont les bibliothèques officielles vérifient la signature sans code spécifique.
  • La livraison est garantie au moins une fois, sans garantie d'ordre, et relancée pendant près de trois jours en cas d'échec.
  • Les webhooks sont en version bêta et font partie du plan Pro, comme le reste de l'API.

Les événements

TypeQuandObjet livré dans data.object
review.createdUn nouvel avis arrive sur un établissement.L'avis (Review)
review.updatedSon auteur modifie la note ou le texte.L'avis
review.deletedL'avis disparaît de la plateforme.L'avis
review.reply_publishedUne réponse est publiée, depuis l'application, l'autopilote, l'API ou directement sur Google.L'avis, avec sa réponse
feedback.createdUn client laisse un retour privé depuis un QR code.Le retour privé (Feedback)
contact.createdUn client entre dans la base clients.Le contact, sans le téléphone (EventContact)
contact.updatedSes coordonnées ou ses consentements changent.Le contact
contact.unsubscribedLe client retire son consentement email ou SMS, y compris par STOP ou lien de désinscription.Le contact
webhook.testVous demandez un événement de test. Il ne s'abonne pas et n'apparaît jamais dans /v1/events.{ "object": "webhook_endpoint", "id": "..." }

Un même fait ne produit qu'un seul événement, quel que soit le chemin qui l'a causé : une réponse publiée depuis l'application puis relue lors de la synchronisation avec Google ne donne qu'un review.reply_published. Le champ origin dit d'où vient le fait :

originSignification
syncSynchronisation avec la plateforme, y compris une réponse saisie directement sur Google.
importImport de l'historique des avis d'un établissement.
appAction faite dans l'application.
autopilotPublication par l'autopilote.
apiÉcriture faite par l'API.
systemTraitement automatique de la plateforme.

La liste des types et celle des origines sont ouvertes : prévoyez un traitement par défaut pour une valeur que vous ne connaissez pas. Un point de réception ne reçoit que les types auxquels il est abonné.

L'import de l'historique d'un établissement peut produire des milliers d'événements review.created d'origine import. Par défaut, un point de réception ne les reçoit pas (skipImportEvents: true) : ils restent lisibles par GET /v1/events.

Créer un point de réception

Il faut une adresse HTTPS publique, sur le port 443 ou 8443. Les adresses IP, les noms internes et les adresses privées sont refusés. Un compte a au plus 10 points de réception, désactivés compris.

Dans l'application

  1. Ouvrez Réglages › Intégration, API & webhooks, onglet Webhooks, puis Ajouter un point de réception. Seuls les propriétaires et les administrateurs y ont accès.
  2. Saisissez l'adresse de réception et cochez les événements suivis.
  3. Laissez cochée la case Ignorer les avis importés lors du rattrapage historique, sauf si votre outil doit recevoir tout l'historique.
  4. Copiez le secret de signature, qui commence par whsec_ : il ne s'affiche qu'une seule fois.

L'adresse d'un point de réception ne se modifie pas : pour en changer, ajoutez-en un nouveau, puis supprimez l'ancien.

Par l'API

POST /v1/webhook-endpoints crée un point de réception avec une clé qui porte le droit webhooks:manage. C'est l'abonnement qu'attendent les outils d'automatisation qui gèrent eux-mêmes leurs webhooks. Les événements de contact portent des données personnelles : s'y abonner exige que la clé porte aussi le droit contacts:read, sinon 403 insufficient_scope. La même règle vaut pour leur lecture par GET /v1/events.

curl https://api.mabellenote.com/v1/webhook-endpoints \
  -H "Authorization: Bearer $MBN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://outil.exemple.com/webhooks/mabellenote",
    "eventTypes": ["review.created", "review.reply_published"],
    "description": "Avis vers le CRM"
  }'
{
  "object": "webhook_endpoint",
  "id": "wh3k6m9p2r5t8v1x4z7b0d3f6h9j2l5n",
  "url": "https://outil.exemple.com/webhooks/mabellenote",
  "description": "Avis vers le CRM",
  "eventTypes": ["review.created", "review.reply_published"],
  "apiVersion": "v1",
  "skipImportEvents": true,
  "status": "active",
  "source": "api",
  "createdAt": "2026-10-25T09:30:00.000Z",
  "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSwq3Bx7zNkE1o="
}

Le secret n'est renvoyé qu'une seule fois : rangez-le aussitôt dans un gestionnaire de secrets. Cette opération n'accepte pas d'en-tête Idempotency-Key, car la réponse rejouée devrait conserver le secret : après une erreur réseau, listez les points de réception avec GET /v1/webhook-endpoints avant d'en recréer un.

SituationRéponse
Adresse non HTTPS, IP, nom interne, adresse privée, autre port422 unprocessable
Déjà 10 points de réception sur le compte409 webhook_endpoint_limit_reached
Compte en lecture seule403 tenant_read_only

GET /v1/webhook-endpoints liste tous les points de réception du compte, créés dans l'application (source: "app") ou par l'API (source: "api"). DELETE /v1/webhook-endpoints/{endpointId} en supprime un, avec son historique de livraisons.

Ce que reçoit votre serveur

Chaque livraison est une requête POST dont le corps est l'événement, en JSON :

POST /webhooks/mabellenote HTTP/1.1
Content-Type: application/json
User-Agent: MaBelleNote-Webhooks/1
webhook-id: ev8k2m4n6p8r0t2v4x6z8b0d2f4h6j8l
webhook-timestamp: 1761384135
webhook-signature: v1,DX+qqa2n8CTLw+sUeFhKkNb/uZsBdhzeQuh1sTFzaNY=
{
  "object": "event",
  "id": "ev8k2m4n6p8r0t2v4x6z8b0d2f4h6j8l",
  "type": "review.created",
  "apiVersion": "v1",
  "createdAt": "2026-10-20T18:42:15.311Z",
  "establishmentId": "kh2n8p5r3t6v9b1c4d7f0g2j5k8m1n3p",
  "origin": "sync",
  "data": {
    "object": {
      "object": "review",
      "id": "jd7f3k2m9x8w1q4z6c5v3b2n1m0l9k8j",
      "establishmentId": "kh2n8p5r3t6v9b1c4d7f0g2j5k8m1n3p",
      "platform": "google",
      "rating": 2,
      "comment": "Service très lent ce soir, dommage car les plats étaient bons.",
      "author": { "name": "Jean D.", "photoUrl": null },
      "createdAt": "2026-10-20T18:42:11.000Z",
      "reply": null
    }
  }
}

L'exemple est abrégé : l'objet livré a exactement la forme décrite dans la référence, figée au moment où l'événement est publié. Un objet supprimé entre-temps est livré sous la forme { "object": "review", "id": "...", "deleted": true }.

En-têteValeur
webhook-idIdentifiant de l'événement, égal à son id. Il ne change pas d'une relance à l'autre.
webhook-timestampHeure d'envoi de cette tentative, en secondes depuis le 1er janvier 1970 (UTC).
webhook-signatureUne ou plusieurs signatures v1,<base64>, séparées par une espace.
User-AgentMaBelleNote-Webhooks/1.

Vérifier la signature

La signature prouve que la requête vient bien de Ma Belle Note et que son corps n'a pas été modifié. Vérifiez-la avant tout traitement, et refusez la requête si elle échoue.

Elle vaut v1, suivi du HMAC-SHA256, encodé en base64, de la chaîne <webhook-id>.<webhook-timestamp>.<corps>, calculé avec la clé obtenue en décodant en base64 la partie du secret qui suit whsec_. Les bibliothèques officielles Standard Webhooks font ce calcul pour vous, comparent les signatures en temps constant et refusent un horodatage écarté de plus de 5 minutes de l'heure de votre serveur, ce qui bloque le rejeu d'une requête interceptée.

Vérifiez le corps brut

La signature porte sur le corps exact, octet pour octet. Passez à la bibliothèque le corps tel qu'il est arrivé, avant tout décodage JSON : un corps décodé puis réencodé change d'espaces ou d'échappements, et la signature ne correspond plus.

Node.js

Avec standardwebhooks et Express (npm install standardwebhooks express), dans un module ES :

import express from "express";
import { Webhook, WebhookVerificationError } from "standardwebhooks";

const webhook = new Webhook(process.env.MBN_WEBHOOK_SECRET);
const processed = new Set();
const app = express();

function handleEvent(event) {
  if (processed.has(event.id)) return;
  processed.add(event.id);
  console.log(event.type, event.data.object.id);
}

app.post("/webhooks/mabellenote", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = webhook.verify(req.body, req.headers);
  } catch (error) {
    if (error instanceof WebhookVerificationError) return res.sendStatus(400);
    throw error;
  }
  res.sendStatus(204);
  handleEvent(event);
});

app.listen(3000);

express.raw garde le corps brut dans req.body, et verify renvoie l'événement décodé une fois la signature validée.

PHP

Avec standard-webhooks/standard-webhooks (composer require standard-webhooks/standard-webhooks, PHP 8.0 ou plus) :

<?php

require __DIR__ . '/vendor/autoload.php';

use StandardWebhooks\Exception\WebhookVerificationException;
use StandardWebhooks\Webhook;

$payload = file_get_contents('php://input');
$headers = array_change_key_case(getallheaders(), CASE_LOWER);

try {
    $event = (new Webhook(getenv('MBN_WEBHOOK_SECRET')))->verify($payload, $headers);
} catch (WebhookVerificationException $e) {
    http_response_code(400);
    exit;
}

file_put_contents(__DIR__ . '/events.ndjson', json_encode($event) . "\n", FILE_APPEND | LOCK_EX);
http_response_code(204);

La bibliothèque PHP cherche les en-têtes en minuscules (webhook-id, webhook-timestamp, webhook-signature) : array_change_key_case les y ramène, quelle que soit la casse transmise par votre serveur web. verify renvoie l'événement sous forme de tableau associatif.

Python

Avec standardwebhooks et Flask (pip install standardwebhooks flask) :

import os

from flask import Flask, request
from standardwebhooks import Webhook, WebhookVerificationError

app = Flask(__name__)
webhook = Webhook(os.environ["MBN_WEBHOOK_SECRET"])
processed = set()


@app.post("/webhooks/mabellenote")
def receive_event():
    try:
        event = webhook.verify(request.get_data(), dict(request.headers))
    except WebhookVerificationError:
        return "", 400
    if event["id"] not in processed:
        processed.add(event["id"])
        print(event["type"], event["data"]["object"]["id"])
    return "", 204

request.get_data() renvoie le corps brut, et la bibliothèque accepte les en-têtes quelle que soit leur casse.

Dans un autre langage

Des bibliothèques officielles existent aussi pour Go, Java, C#, Ruby, Rust et Elixir : le dépôt Standard Webhooks les liste. Si vous écrivez la vérification vous-même, suivez la formule ci-dessus, acceptez la requête dès qu'une des signatures v1 correspond, comparez en temps constant et contrôlez l'horodatage.

Répondre vite, traiter ensuite

  • Une livraison réussit quand votre serveur répond par un statut 2xx en moins de 10 secondes. Tout autre statut, une redirection (jamais suivie), un délai dépassé ou une erreur réseau compte comme un échec.
  • Répondez dès la signature vérifiée, puis traitez l'événement à part, par exemple dans une file : un traitement long ferait échouer la livraison, qui serait relancée.
  • Dédoublonnez par webhook-id : la livraison au moins une fois signifie qu'un même événement peut arriver deux fois, par exemple quand votre réponse s'est perdue. Gardez la trace des identifiants traités dans votre base, pas seulement en mémoire comme dans les exemples ci-dessus.
  • Ne vous fiez pas à l'ordre d'arrivée : comparez createdAt, ou relisez l'objet par l'API (GET /v1/reviews/{reviewId}) quand seul son dernier état compte.

Tester la réception

L'événement de test vérifie de bout en bout que votre serveur reçoit la livraison et valide sa signature.

  • Dans l'application : menu du point de réception, Envoyer un événement de test.
  • Par l'API : POST /v1/webhook-endpoints/{endpointId}/test, qui envoie aussitôt un événement webhook.test signé comme une vraie livraison et renvoie le résultat.
curl -X POST https://api.mabellenote.com/v1/webhook-endpoints/wh3k6m9p2r5t8v1x4z7b0d3f6h9j2l5n/test \
  -H "Authorization: Bearer $MBN_API_KEY"
{
  "object": "webhook_test",
  "delivered": true,
  "status": 200,
  "errorKind": null,
  "durationMs": 182
}
errorKindCause
http_statusVotre serveur a répondu par un statut hors 2xx, donné par status.
redirectVotre serveur a répondu par une redirection, qui n'est pas suivie.
timeoutPas de réponse en 10 secondes.
networkConnexion impossible : nom introuvable, certificat refusé, etc.
blocked_destinationL'adresse se résout vers une adresse refusée, par exemple privée.

L'événement de test n'est ni relancé, ni enregistré dans les livraisons, ni journalisé dans GET /v1/events.

Relances et désactivation automatique

Une livraison en échec est relancée selon ce calendrier, à compter de l'échec précédent :

TentativeDélai
21 minute
35 minutes
430 minutes
52 heures
66 heures
712 heures
824 heures
924 heures

Après la neuvième tentative, près de 70 heures après la première, la livraison est abandonnée. Les livraisons d'un même point de réception partent l'une après l'autre : un point de réception en panne ne retarde pas les autres.

Quand un point de réception n'a reçu aucune livraison réussie pendant 72 heures et a enchaîné au moins 9 échecs, il est désactivé automatiquement (status: "disabled_auto"). Les propriétaires et les administrateurs du compte sont prévenus par email, et les nouveaux événements ne lui sont plus envoyés.

Quand le compte quitte le plan Pro, les envois sont mis en pause sans rien supprimer : les événements continuent d'être journalisés et les livraisons reprennent au retour au plan Pro. Celles restées en attente plus de 72 heures sont alors abandonnées.

Rejouer des livraisons

Le rejeu se fait dans l'application, onglet Webhooks, où chaque point de réception garde ses livraisons des 30 derniers jours avec leur état, leurs tentatives et le dernier code reçu :

  • Une livraison : Voir les livraisons, puis Rejouer sur la livraison voulue.
  • Après une panne : pour un point de réception désactivé automatiquement, Réactiver et rejouer les livraisons manquées le réactive et renvoie les livraisons abandonnées pendant la panne.

Un point de réception désactivé doit être réactivé avant tout rejeu. Un rejeu renvoie la même charge utile, avec le même webhook-id : votre dédoublonnage s'applique.

Pour rattraper des événements depuis votre outil, sans passer par l'application, lisez le journal d'événements : GET /v1/events?after=<position> renvoie tous les événements du compte des 30 derniers jours, dans l'ordre de leur publication, avec la même charge utile que les webhooks (droit events:read). Le guide de pagination et de synchronisation détaille son usage.

Renouveler le secret

Dans le menu du point de réception, Renouveler le secret crée un nouveau secret, affiché une seule fois. L'ancien reste valide 24 heures : pendant ce délai, chaque livraison porte deux signatures dans webhook-signature, une par secret.

  1. Renouvelez le secret et copiez le nouveau.
  2. Déployez-le dans votre outil, dans les 24 heures.
  3. Les bibliothèques Standard Webhooks acceptent la livraison dès qu'une des signatures correspond : rien d'autre à changer.

Renouvelez le secret dès que vous soupçonnez une fuite, ou quand une personne qui y avait accès quitte votre équipe.

Mis à jour le 25 octobre 2026