Aller au contenu

Vérifier les signatures

Toute personne qui connaît l’URL de votre endpoint peut y envoyer des requêtes. Vérifiez toujours la signature avant de faire confiance à un payload.

Chaque distribution porte :

webhook-id: 0192e1c4-0000-7000-8000-0000000000cc
webhook-timestamp: 1761228131
webhook-signature: v1,K5oZfzN0mYI+Hx3Yb0v8j3H5kq2tqZ2dA8s0r6sF9Vw=

La signature est le HMAC-SHA256 en base64 de "{webhook-id}.{webhook-timestamp}.{raw body}", avec pour clé les octets du secret : prenez votre signing_secret, retirez le préfixe whsec_ et décodez le reste en base64. Rejetez les requêtes dont l’horodatage s’écarte de plus de 5 minutes de votre horloge afin d’empêcher les rejeux.

Le SDK SpacePost vérifie avec WebCrypto et renvoie l’événement analysé. Il lève une exception si la signature ou l’horodatage est invalide.

webhook-handler.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
/** Works as a Bun, Deno, Cloudflare Workers or Next.js route handler. */
export async function POST(request: Request): Promise<Response> {
const payload = await request.text(); // the RAW body: do not parse it first
try {
const event = await spacepost.webhooks.verify({
payload,
headers: request.headers,
secret: process.env.SPACEPOST_WEBHOOK_SECRET!,
});
switch (event.type) {
case "email.bounced":
case "email.complained":
console.warn("Stop mailing", event.data.to);
break;
case "email.delivered":
console.log("Delivered", event.data.email_id);
break;
}
return new Response("ok");
} catch {
return new Response("invalid signature", { status: 400 });
}
}

Les en-têtes peuvent être un objet Headers ou un objet simple (par exemple req.headers d’Express). Avec Express, montez express.raw({ type: "application/json" }) sur la route du webhook pour que req.body soit un Buffer.

Avec le paquet standardwebhooks :

import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.SPACEPOST_WEBHOOK_SECRET!); // "whsec_..." is accepted
const event = wh.verify(rawBody, {
"webhook-id": req.headers["webhook-id"] as string,
"webhook-timestamp": req.headers["webhook-timestamp"] as string,
"webhook-signature": req.headers["webhook-signature"] as string,
});

Avec svix (ou le vérificateur du paquet resend) : utilisez les en-têtes svix-*, que SpacePost envoie aussi.

import { Webhook } from "svix";
const wh = new Webhook(process.env.SPACEPOST_WEBHOOK_SECRET!);
const event = wh.verify(rawBody, {
"svix-id": req.headers["svix-id"] as string,
"svix-timestamp": req.headers["svix-timestamp"] as string,
"svix-signature": req.headers["svix-signature"] as string,
});

La vérification des webhooks du paquet officiel resend prend les mêmes en-têtes Standard Webhooks : elle fonctionne donc avec votre signing_secret SpacePost.

Le signing_secret est fixe pour chaque webhook. Pour le renouveler, créez un nouveau webhook avec le même endpoint, faites accepter le nouveau secret par votre gestionnaire (essayez les deux pendant la transition), puis supprimez l’ancien webhook.

Envoyez un e-mail à delivered@test.spacepost.dev avec l’URL d’un tunnel enregistrée comme endpoint. Voir Tests.