Salta ai contenuti

Verificare le firme

Chiunque venga a conoscenza dell’URL del tuo endpoint può inviargli richieste. Verifica sempre la firma prima di fidarti di un payload.

Ogni consegna include:

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

La firma è l’HMAC-SHA256 in base64 di "{webhook-id}.{webhook-timestamp}.{raw body}", con chiave i byte del secret: prendi il tuo signing_secret, togli il prefisso whsec_ e decodifica il resto da base64. Rifiuta le richieste il cui timestamp si discosta di più di 5 minuti dal tuo orologio, per impedire i replay.

L’SDK di SpacePost verifica con WebCrypto e restituisce l’evento analizzato. Lancia un’eccezione se la firma o il timestamp non sono validi.

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 });
}
}

Gli header possono essere un oggetto Headers o un oggetto semplice (ad esempio req.headers di Express). Con Express, monta express.raw({ type: "application/json" }) sulla route del webhook in modo che req.body sia un Buffer.

Con il pacchetto 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,
});

Con svix (o il verificatore del pacchetto resend): usa gli header svix-*, che SpacePost invia ugualmente.

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 verifica dei webhook del pacchetto ufficiale resend accetta gli stessi header Standard Webhooks, quindi funziona con il signing_secret di SpacePost.

Il signing_secret è fisso per ogni webhook. Per ruotarlo, crea un nuovo webhook con lo stesso endpoint, fai in modo che il tuo gestore accetti il nuovo secret (provali entrambi durante il passaggio), poi elimina il vecchio webhook.

Invia un’email a delivered@test.spacepost.dev con l’URL di un tunnel registrato come endpoint. Vedi Test.