Todos os artigos
Envelopes de eventos de entrega, rejeição e reclamação de e-mail passando por um túnel assinado até uma rota Next.js no localhost.
ResendNext.jsemail webhookslocalhost

Testar webhooks do Resend localmente com Next.js

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-id identifica exclusivamente uma entrega e é a melhor chave de idempotência.
  • svix-timestamp vincula a assinatura a um horário, permitindo que o verificador rejeite solicitações obsoletas fora de sua tolerância.
  • svix-signature pode 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

  1. Corre npm run dev e confirme se o aplicativo escuta na porta 3000.
  2. Começar npx portpreview 3000 em outro terminal.
  3. Em Resend, crie um webhook cujo endpoint seja https://YOUR-TUNNEL.portpreview.dev/api/webhooks/resend.
  4. Selecione apenas os eventos de e-mail que seu aplicativo processa.
  5. Copie o segredo de assinatura do endpoint para RESEND_WEBHOOK_SECRET e reinicie Next.js para carregar a variável.
  6. 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_KEY ou 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.

Perguntas frequentes

Como faço para testar webhooks Resend localmente em Next.js?
Crie uma rota App Router POST, verifique o corpo bruto com os cabeçalhos Svix e o segredo de assinatura do terminal, exponha a porta 3000 por meio de HTTPS e registre essa URL pública em Resend.
Um webhook Resend deve usar request.json() em Next.js?
Não antes da verificação. Leia await request.text() para que os bytes assinados permaneçam inalterados, verifique com Svix e use o evento verificado retornado pelo SDK.
O segredo de assinatura do webhook Resend é igual à chave de API?
Não. A chave API autoriza o envio de solicitações. Cada endpoint do webhook possui um segredo de assinatura usado para verificar eventos recebidos; armazene ambos separadamente.
Como evito o processamento duplicado de webhook Resend?
Armazene Svix-id sob uma restrição exclusiva e aplique o evento na mesma transação. Não desduplique apenas por ID de e-mail porque um e-mail tem vários eventos válidos.