Webhooks
Onze événements, une signature HMAC sur le corps brut, huit tentatives avec backoff - et un événement qui garde un frontend headless à jour.
Un webhook, c'est la façon dont cmssy dit à votre application que quelque chose a changé, au lieu que votre application demande. Onze événements, une forme de livraison, et un détail plus important que le reste : content.changed transforme une publication en page en ligne sur un frontend mis en cache.
Les événements
list_webhook_event_types est la liste blanche faisant autorité - lisez-la plutôt que de deviner un nom. Aujourd'hui elle en renvoie onze :
- Commandes -
order.created,order.partially_paid,order.paid,order.partially_refunded,order.refunded,order.canceled,order.fulfilled,order.returned,order.edited,order.pipeline_changed. - Contenu -
content.changed, un événement unique et large pour les pages, enregistrements, formulaires et réglages de l'espace de travail.
content.changed est volontairement large
Il n'existe pas de page.published. Un seul événement couvre toute mutation de contenu, et la charge utile dit ce qui s'est passé :
{
"kind": "page", // page | record | form | settings
"action": "published", // published | unpublished | created | updated | deleted
"ids": ["6a63..."],
"slug": "/pricing", // pages uniquement
"modelSlug": null // enregistrements uniquement
}Abonnez-vous une fois et traitez toute paire comme « invalider ». C'est l'intérêt de cette largeur : un nouveau type de sujet ajouté plus tard vous parvient sans nouvel abonnement.
Les réglages sont un de ces types. Changer l'identité visuelle ou les langues activées émet kind: "settings" avec action: "updated", un tableau ids vide et un slug nul : rien ne désigne une page précise, donc invalidez tout l'arbre. Sans cela, un nouveau logo ou une langue fraîchement activée attend la fin de votre fenêtre de cache.
Deux cas limites à coder. Une opération en masse dont l'ensemble touché est inconnu ou dépasse 100 envoie un tableau ids vide : traitez-le comme « tout invalider ». Et pour une suppression groupée de pages, slug est la racine du sous-arbre supprimé ; les descendants listés dans ids vivaient sous d'autres slugs.
Garder un frontend en cache à jour
Publier ne déploie pas, et ne contourne pas non plus votre cache. Une page en revalidate = 3600 continue de servir l'ancienne copie jusqu'à une heure tant que rien ne l'invalide. Ce quelque chose est un webhook content.changed pointé vers une route de revalidation :
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
import { NextResponse, type NextRequest } from "next/server";
export async function POST(request: NextRequest) {
const secret = process.env.CMSSY_REVALIDATE_SECRET;
const provided =
request.nextUrl.searchParams.get("secret") ??
request.headers.get("x-revalidate-secret");
if (!secret || provided !== secret) {
return NextResponse.json({ revalidated: false }, { status: 401 });
}
const { data } = (await request.json().catch(() => ({}))) as {
data?: { subject?: { slug?: string | null } };
};
const slug = data?.subject?.slug;
// Pas de slug = ensemble inconnu : invalider tout l'arbre.
if (slug) revalidatePath(slug);
else revalidatePath("/", "layout");
return NextResponse.json({ revalidated: true, path: slug ?? "/" });
}Si vos URL portent un préfixe de langue, revalidez aussi les chemins localisés - chacun est mis en cache séparément.
À quoi ressemble une livraison
Chaque livraison est un POST avec content-type: application/json et ce corps :
{
"id": "6a64...", // l'id du point de terminaison
"event": "content.changed",
"createdAt": "2026-07-25T18:30:00.000Z",
"data": { "workspaceId": "...", "subject": { } }
}Trois en-têtes l'accompagnent : x-cmssy-event, x-cmssy-webhook-id et x-cmssy-signature.
Vérifier la signature
L'en-tête de signature vaut t=<unix-ms>,v1=<hex>, où l'hex est un HMAC-SHA256 sur <t>.<corps brut> avec le secret du point de terminaison comme clé. Signez le corps brut : resérialiser du JSON parsé change les octets et casse la comparaison.
import { createHmac, timingSafeEqual } from "crypto";
export function verifyCmssySignature(
header: string | null,
rawBody: string,
secret: string,
): boolean {
const parts = Object.fromEntries(
(header ?? "").split(",").map((p) => p.split("=") as [string, string]),
);
if (!parts.t || !parts.v1) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
// Rejetez aussi un horodatage ancien : une livraison capturée est rejouable.
return a.length === b.length && timingSafeEqual(a, b);
}Relances
cmssy attend 5 secondes une réponse. Tout ce qui n'est pas 2xx - ou un délai dépassé - est relancé, jusqu'à 8 tentatives, avec un backoff de 1 min, 5 min, 15 min, 30 min, 1 h, 2 h, 4 h. Un échec permanent s'arrête plus tôt.
Deux conséquences pour votre handler. Il doit être rapide : répondez 2xx puis faites le travail, sinon un point de terminaison lent devient une tempête de relances. Et il doit être idempotent : une livraison déjà traitée peut revenir, avec les mêmes id et createdAt.
list_webhook_deliveries montre les tentatives récentes avec leur statut et leur code de réponse ; les livraisons sont conservées 30 jours.
Gérer les points de terminaison
create_webhook- renvoie le point de terminaison et son secret, une seule fois. Stockez-le tout de suite ; il ne reviendra jamais.rotate_webhook_secret- renvoie un nouveau secret, une seule fois également. Les anciennes signatures cessent immédiatement d'être valides.update_webhook- mise à jour partielle ; passezenabledpour suspendre sans supprimer.list_webhooks,delete_webhook,list_webhook_deliveries.
Jusqu'à 20 points de terminaison par espace de travail. Les URL doivent être en https en production, et les cibles privées sont refusées : localhost, *.local, *.internal, link-local et plages RFC 1918. Un webhook capable d'atteindre votre réseau interne est une surface SSRF, pas une fonctionnalité.
Étapes suivantes
- Serveur MCP - les outils qui créent et inspectent les points de terminaison.
- Aperçu brouillon - publication, cache et ce que signifie une page périmée.
- Jetons API - l'identifiant derrière ces outils.