Para testar webhooks Resend localmente em Next.js, crie uma rota App Router POST que leia o corpo bruto, verifique seus cabeçalhos Svix com seu segredo de assinatura Resend e registre uma URL de túnel HTTPS no painel Resend. Envie um e-mail através de Resend e depois lide com o real email.sent, email.delivered, email.bounced, ou email.complained eventos no localhost.
O que um webhook Resend diz ao seu aplicativo
Uma resposta da API informando que um e-mail foi aceito não é prova de que ele chegou ao destinatário. A entrega acontece de forma assíncrona. Os webhooks Resend permitem que seu aplicativo atualize o estado da mensagem, suprima endereços incorretos, relate devoluções e reaja a reclamações após a conclusão da solicitação de envio original. O documentação oficial do webhook Resend lista tipos de eventos e configuração do painel.
Um teste de webhook local deve cobrir toda a máquina de estado, não apenas se um POST atinge sua rota. Correlacione o ID de e-mail de cada evento com o registro criado no envio. Trate os estados como transições: aceito, enviado, entregue, atrasado, devolvido, reclamado, aberto ou clicado quando aplicável. Uma duplicata posterior não deve substituir um estado mais útil ou acionar o mesmo alerta duas vezes.
Crie a rota Next.js App Router
Instale o verificador mantido para o formato de assinatura:
npm install svix
Em seguida, crie uma rota de tempo de execução do nó. Resend assina o corpo original, então use request.text() exatamente uma vez antes de analisar.
// app/api/webhooks/resend/route.ts
import { Webhook } from 'svix';
export const runtime = 'nodejs';
export async function POST(request: Request) {
const payload = await request.text();
const headers = {
'svix-id': request.headers.get('svix-id') ?? '',
'svix-timestamp': request.headers.get('svix-timestamp') ?? '',
'svix-signature': request.headers.get('svix-signature') ?? '',
};
let event: ResendEvent;
try {
const webhook = new Webhook(process.env.RESEND_WEBHOOK_SECRET!);
event = webhook.verify(payload, headers) as ResendEvent;
} catch {
return Response.json({ error: 'Invalid webhook signature' }, { status: 400 });
}
await enqueueResendEvent({
deliveryId: headers['svix-id'],
event,
});
return Response.json({ received: true });
}
O segredo de assinatura pertence a esse endpoint do webhook e normalmente começa com um prefixo específico do provedor. Copie-o das configurações do webhook Resend para um arquivo de ambiente local ignorado, como .env.local. Não é a chave de API Resend usada para enviar email.
Por que os três cabeçalhos Svix são importantes
svix-ididentifica exclusivamente uma entrega e é a melhor chave de idempotência.svix-timestampvincula a assinatura a um horário, permitindo que o verificador rejeite solicitações obsoletas fora de sua tolerância.svix-signaturepode conter uma ou mais assinaturas versionadas usadas para autenticar o corpo.
Não implemente este protocolo dividindo strings de cabeçalho, a menos que você tenha um motivo convincente. O SDK lida com codificação, múltiplas assinaturas e verificações de carimbo de data/hora. Resend recomenda explicitamente o uso do segredo de assinatura e desses cabeçalhos para verificação. Quanto mais profundo guia de verificação de assinatura explica por que bytes brutos e verificações seguras de tempo são importantes.
Exponha Next.js e registre o endpoint
- Corre
npm run deve confirme se o aplicativo escuta na porta 3000. - Começar
npx portpreview 3000em outro terminal. - Em Resend, crie um webhook cujo endpoint seja
https://YOUR-TUNNEL.portpreview.dev/api/webhooks/resend. - Selecione apenas os eventos de e-mail que seu aplicativo processa.
- Copie o segredo de assinatura do endpoint para
RESEND_WEBHOOK_SECRETe reinicie Next.js para carregar a variável. - Envie uma mensagem usando um domínio verificado e inspecione os eventos que chegam à rota local.
Mantenha a URL pública estável para a sessão. Se a origem do túnel for alterada, edite o terminal Resend antes de testar novamente. Um endpoint configurado com uma URL antiga não pode alcançar seu novo processo, mesmo que o próprio localhost esteja íntegro.
Use um despachante de eventos digitado
As cargas úteis do webhook devem entrar em um despachante restrito. Valide campos obrigatórios e torne observáveis tipos de eventos não reconhecidos sem tratá-los como falhas de servidor.
async function processEvent(event: ResendEvent) {
switch (event.type) {
case 'email.delivered':
await markDelivered(event.data.email_id, event.created_at);
break;
case 'email.bounced':
await markBounced(event.data.email_id, event.data.bounce?.message);
await suppressIfPermanent(event.data);
break;
case 'email.complained':
await suppressRecipients(event.data.to);
await alertCompliance(event.data.email_id);
break;
default:
await recordUnhandledEvent(event);
}
}
Mantenha os tipos de carga alinhados com o esquema Resend atual em vez de assumir que cada evento possui dados idênticos. Por exemplo, detalhes de devoluções e listas de destinatários podem ser relevantes apenas em determinados eventos. Salve o tipo de evento, o ID de e-mail do provedor, o carimbo de data/hora do evento e uma carga mínima editada para investigações de suporte.
Torne o manuseio idempotente antes de tentar novas tentativas
Os sistemas Webhook fornecem comportamento de entrega pelo menos uma vez na prática. Um tempo limite pode ocorrer após a confirmação do banco de dados, mas antes que o provedor receba sua resposta 200. O provedor então tenta novamente uma solicitação que você já aplicou. Usar svix-id como uma chave de entrega exclusiva e insira-a na mesma transação da mudança de estado.
await db.transaction(async (tx) => {
const inserted = await tx.webhookDelivery.insertOnce({
provider: 'resend',
deliveryId,
});
if (!inserted) return;
await applyEmailEvent(tx, event);
});
Não desduplique apenas por ID de e-mail porque um e-mail recebe legitimamente vários tipos de eventos. Dependendo do seu modelo de dados, mantenha uma chave exclusiva no nível da entrega e regras de transição de estado. Leia padrões de repetição e idempotência do webhook antes de conectar eventos a faturamento, supressão ou notificações de clientes.
Retorne rapidamente sem perder o evento
A verificação da assinatura é apropriada no caminho da solicitação; o trabalho empresarial lento não é. Persista ou enfileire o evento verificado e retorne 2xx. Se você retornar antes de qualquer gravação durável, uma falha no processo poderá perder o evento. Se você esperar por várias APIs remotas, seu endpoint poderá atingir o tempo limite e solicitar novas tentativas. Uma tabela de caixa de entrada de banco de dados costuma ser o design local e de produção mais simples.
Gere eventos de teste úteis
Enviado e entregue
Envie para um endereço que você controla de um domínio verificado. Registre o ID do e-mail retornado pela API de envio e confirme se os eventos recebidos atualizam a mesma linha. O tempo de entrega varia de acordo com o servidor destinatário, portanto, não presuma que os eventos chegam imediatamente ou em uma sequência simplista.
Saltos
Use endereços de teste documentados ou recursos de teste de Resend em vez de inventar tráfego para domínios não relacionados. Verifique se as falhas permanentes suprimem emails futuros enquanto as condições temporárias seguem sua política de novas tentativas. Não suprima automaticamente todos os eventos atrasados.
Reclamações
O tratamento de reclamações é tanto a capacidade de entrega quanto a lógica de conformidade. Certifique-se de que um webhook repetido não crie alertas repetidos e garanta que o destinatário afetado seja excluído de campanhas posteriores de acordo com sua política.
Solucionar problemas de falhas do webhook Resend
A verificação de assinatura sempre falha
Confirme se o segredo de assinatura do endpoint (e não a chave de API) está carregado. Usar await request.text(), não analise e restrinja JSON e passe todos os três cabeçalhos Svix com seus valores exatos. Reinicie o servidor de desenvolvimento após alterar .env.local.
A rota retorna 404 ou 405
Os arquivos de rota App Router devem ser nomeados route.ts abaixo dos segmentos de URL pretendidos e exporte POST. Verifique se o middleware reescreve a solicitação de túnel em uma localidade ou página de login. Teste o URL público com curl e inspecione a resposta real.
Resend mostra novas tentativas apesar do processamento bem-sucedido
Verifique se cada ramificação bem-sucedida retorna 2xx imediatamente. Erros gerados após uma atualização do banco de dados podem produzir uma nova tentativa 500 e uma duplicata. Torne o processamento transacional e idempotente e, em seguida, inspecione a latência da resposta.
Os eventos chegam, mas não podem ser vinculados a um e-mail
Persista o ID de e-mail do provedor da resposta de envio Resend original. Não confie em linhas de assunto ou endereços de destinatários como identificadores. Esses campos não são únicos nem estáveis o suficiente para correlação.
As capturas reproduzidas falham na verificação do carimbo de data/hora
Isso é esperado ao reproduzir uma solicitação antiga assinada através do verificador normal: seu carimbo de data/hora pode estar fora da tolerância permitida. Prefira a reentrega do provedor, quando disponível. Para testes isolados de lógica de negócios, verifique uma vez, salve um evento higienizado e teste o despachante separadamente. O guia de repetição explica esse limite.
Segurança e privacidade para testes de eventos de email
- Nunca exponha
RESEND_API_KEYou o segredo de assinatura do endpoint na origem, pacotes do navegador, capturas de tela ou logs de solicitação. - Verifique antes de analisar ou persistir o evento.
- Edite destinatários, assuntos, cabeçalhos e metadados de mensagens de capturas de túnel compartilhadas.
- Aplicar limites de retenção a cargas brutas de webhook; armazene apenas o que o suporte e a conformidade exigem.
- Use um segredo de endpoint local separado da produção e alterne-o quando o endpoint de teste for excluído.
O design final deve funcionar de forma idêntica após a implantação: endpoint HTTPS público, verificação de corpo bruto, idempotência durável, reconhecimento rápido e tratamento de estado assíncrono. Para obter detalhes do corpo bruto específicos de App Router, consulte o Guia de host local do webhook Next.js.
