Webhooks
Un webhook envoie une requête HTTP signée à ton serveur dès qu'un événement se produit sur tes pages, tes équipes ou tes demandes de support.
Sommaire
Créer un webhook
Ajoute un webhook dans Réglages, Clés API et webhooks, ou avec POST /api/v1/me/webhooks (portée webhooks:write). Choisis l'adresse en https, les événements et, si tu es admin d'une équipe, l'équipe concernée. Le secret de signature n'est affiché qu'une fois : garde le dans les variables d'environnement de ton serveur. Le nombre de webhooks dépend de ton palier (tableau des limites du guide).
curl -X POST https://mosavo.fr/api/v1/me/webhooks \
-H "Authorization: Bearer msk_your_key" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/webhooks/mosavo","events":["page.published","message.received"]}'Événements
Chaque webhook ne reçoit que les événements cochés. Un webhook d'équipe reçoit les événements des pages et des membres de l'équipe ; un webhook personnel ceux de tes propres pages et de tes demandes de support. test.ping part uniquement avec le bouton « Envoyer un test ».
page.publishedPage publiée{ "page": { "id": "clp0example", "slug": "ada", "title": "Ada Lovelace", "url": "https://mosavo.fr/ada" } }page.unpublishedPage dépubliée{ "page": { "id": "clp0example", "slug": "ada", "title": "Ada Lovelace" } }page.updatedRéglages de page modifiés{ "page": { "id": "clp0example", "slug": "ada", "title": "Ada Lovelace" }, "changes": [ "title", "theme" ] }block.createdBloc ajouté{ "page": { "id": "clp0example", "slug": "ada", "title": "Ada Lovelace" }, "block": { "id": "clb0example", "type": "LINK" } }block.updatedBloc modifié{ "page": { "id": "clp0example", "slug": "ada", "title": "Ada Lovelace" }, "block": { "id": "clb0example", "type": "LINK" } }block.deletedBloc supprimé{ "page": { "id": "clp0example", "slug": "ada", "title": "Ada Lovelace" }, "block": { "id": "clb0example", "type": "LINK" } }message.receivedMessage reçu{ "page": { "id": "clp0example", "slug": "ada", "title": "Ada Lovelace" }, "message": { "id": "clm0example", "blockId": "clb1example", "name": "Grace", "email": "[email protected]", "subject": "Hello", "body": "I love your page.", "receivedAt": "2026-11-01T12:00:00.000Z" } }follower.addedNouvel abonné{ "page": { "id": "clp0example", "slug": "ada", "title": "Ada Lovelace" }, "follower": { "id": "clx1example", "pseudo": "grace" }, "followers": 12 }follower.removedAbonné parti{ "page": { "id": "clp0example", "slug": "ada", "title": "Ada Lovelace" }, "follower": { "id": "clx1example", "pseudo": "grace" }, "followers": 11 }team.member_joinedMembre arrivé{ "team": { "id": "clt0example", "name": "Studio" }, "member": { "id": "clx1example", "pseudo": "grace", "role": "EDITOR" } }team.member_leftMembre parti{ "team": { "id": "clt0example", "name": "Studio" }, "member": { "id": "clx1example", "pseudo": "grace" }, "removedBy": "self" }ticket.repliedRéponse du support{ "ticket": { "reference": "MOS-4F2K9", "subject": "Custom domain", "status": "PENDING" }, "reply": { "body": "Your domain is now verified.", "repliedAt": "2026-11-01T12:00:00.000Z" } }test.pingTest{ "message": "This is a test event sent from Mosavo.", "webhookId": "clw0example" }
Format de la requête
Mosavo envoie un POST avec un corps JSON avec les champs id, event, createdAt et data. id identifie l'événement et reste le même lors d'un renvoi : sers t'en pour ignorer les doublons. En têtes : Mosavo-Event, Mosavo-Delivery (identifiant de la livraison), Mosavo-Signature et User-Agent: Mosavo-Webhooks/1.0. Réponds par un code 2xx en moins de 10 secondes ; les redirections ne sont pas suivies et seuls les 64 premiers Ko de la réponse sont lus.
POST /webhooks/mosavo HTTP/1.1
Content-Type: application/json
User-Agent: Mosavo-Webhooks/1.0
Mosavo-Event: page.published
Mosavo-Delivery: cld0example
Mosavo-Signature: t=1793361660,v1=5f0c…
{
"id": "evt_3f9a1c",
"event": "page.published",
"createdAt": "2026-11-01T12:00:00.000Z",
"data": {
"page": {
"id": "clp0example",
"slug": "ada",
"title": "Ada Lovelace",
"url": "https://mosavo.fr/ada"
}
}
}Vérifier la signature
Mosavo-Signature vaut t=TIMESTAMP,v1=SIGNATURE. La signature est le HMAC SHA 256, en hexadécimal, de la chaîne timestamp.corps avec ton secret. Calcule la sur le corps brut, avant tout décodage JSON, compare en temps constant et refuse un horodatage de plus de 5 minutes.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto"
import express from "express"
const secret = process.env.MOSAVO_WEBHOOK_SECRET
function verify(header, rawBody) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")))
const timestamp = Number(parts.t)
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest()
const given = Buffer.from(parts.v1 ?? "", "hex")
return given.length === expected.length && timingSafeEqual(given, expected)
}
const app = express()
app.post("/webhooks/mosavo", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.get("Mosavo-Signature") ?? "", req.body.toString("utf8"))) return res.sendStatus(400)
const event = JSON.parse(req.body)
console.log(event.event, event.data)
res.sendStatus(204)
})Python
import hashlib, hmac, json, os, time
from flask import Flask, request, abort
secret = os.environ["MOSAVO_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
def verify(header: str, raw_body: bytes) -> bool:
parts = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
try:
timestamp = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - timestamp) > 300:
return False
expected = hmac.new(secret, f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
@app.post("/webhooks/mosavo")
def mosavo():
if not verify(request.headers.get("Mosavo-Signature", ""), request.get_data()):
abort(400)
event = json.loads(request.get_data())
print(event["event"], event["data"])
return "", 204Nouvelles tentatives
Une livraison réussit avec un code 2xx. Sinon (erreur, délai dépassé, redirection, code 4xx ou 5xx), Mosavo réessaie après 1 min, 5 min, 30 min, 2 h, 6 h, 12 h puis 24 h, soit 8 tentatives au total, puis la marque en échec. Le journal des 50 dernières livraisons, conservé 30 jours, permet de voir la réponse et de renvoyer une livraison.
2. +1 min
3. +5 min
4. +30 min
5. +2 h
6. +6 h
7. +12 h
8. +24 hDésactivation automatique
Après 20 échecs d'affilée, le webhook se désactive et tu reçois une notification. Corrige ton serveur, envoie un test, puis réactive le : le compteur repart de zéro. L'équipe Mosavo peut aussi désactiver un webhook abusif ; il ne peut alors être réactivé que par le support.
Sécurité
Les adresses doivent être en https et publiques : les adresses locales, privées ou réservées sont refusées à l'enregistrement et à chaque envoi, même après une résolution DNS. Le secret est stocké chiffré et n'est jamais renvoyé par l'API ; régénère le s'il a fuité.