Pular para o conteúdo

Verificar assinaturas

Qualquer pessoa que descubra a URL do seu endpoint pode enviar requisições a ele. Sempre verifique a assinatura antes de confiar em um payload.

Cada entrega traz:

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

A assinatura é o HMAC-SHA256 em base64 de "{webhook-id}.{webhook-timestamp}.{corpo bruto}", usando como chave os bytes do segredo: pegue o seu signing_secret, remova o prefixo whsec_ e decodifique o restante em base64. Rejeite requisições cujo horário difira em mais de 5 minutos do seu relógio, para impedir replays.

O SDK do SpacePost verifica com WebCrypto e retorna o evento já interpretado. Ele lança uma exceção se a assinatura ou o horário forem inválidos.

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

Os cabeçalhos podem ser um objeto Headers ou um objeto simples (por exemplo, o req.headers do Express). Com o Express, monte express.raw({ type: "application/json" }) na rota do webhook para que req.body seja um Buffer.

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

Com o svix (ou o verificador do pacote resend): use os cabeçalhos svix-*, que o SpacePost também envia.

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

A verificação de webhooks do pacote oficial resend usa os mesmos cabeçalhos do Standard Webhooks, então funciona com o signing_secret do seu SpacePost.

O signing_secret é fixo por webhook. Para rotacioná-lo, crie um novo webhook com o mesmo endpoint, faça seu handler aceitar o novo segredo (tente os dois durante a transição) e depois exclua o webhook antigo.

Envie um e-mail para delivered@test.spacepost.dev com a URL de um túnel registrada como seu endpoint. Veja Testes.