Zum Inhalt springen

Signaturen prüfen

Jeder, der die URL deines Endpoints kennt, kann an ihn senden. Prüfe immer die Signatur, bevor du einer Payload vertraust.

Jede Zustellung enthält:

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

Die Signatur ist der Base64-kodierte HMAC-SHA256 von "{webhook-id}.{webhook-timestamp}.{raw body}", mit den Bytes des Secrets als Schlüssel: Nimm dein signing_secret, entferne das Präfix whsec_ und dekodiere den Rest per Base64. Lehne Anfragen ab, deren Zeitstempel mehr als 5 Minuten von deiner Uhr abweicht, um Replays zu verhindern.

Das SpacePost SDK prüft mit WebCrypto und gibt das geparste Event zurück. Es wirft eine Exception, wenn Signatur oder Zeitstempel ungültig sind.

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

Die Header können ein Headers-Objekt oder ein einfaches Objekt sein (zum Beispiel req.headers von Express). Binde bei Express express.raw({ type: "application/json" }) an die Webhook-Route, damit req.body ein Buffer ist.

Mit dem Paket 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,
});

Mit svix (oder dem Verifier des Pakets resend): Verwende die svix-*-Header, die SpacePost ebenfalls sendet.

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

Die Webhook-Verifizierung des offiziellen Pakets resend nimmt dieselben Standard-Webhooks-Header entgegen und funktioniert daher mit deinem SpacePost-signing_secret.

Das signing_secret ist pro Webhook fest. Zum Rotieren erstellst du einen neuen Webhook mit demselben Endpoint, stellst deinen Handler so um, dass er das neue Secret akzeptiert (probiere beide während der Umstellung), und löschst dann den alten Webhook.

Sende eine E-Mail an delivered@test.spacepost.dev, wobei eine Tunnel-URL als dein Endpoint registriert ist. Siehe Testen.