Pour tester les webhooks Resend localement dans Next.js, créez une route App Router POST qui lit le corps brut, vérifiez ses en-têtes Svix avec votre secret de signature Resend et enregistrez une URL de tunnel HTTPS dans le tableau de bord Resend. Envoyez un e-mail via Resend, puis gérez le réel email.sent, email.delivered, email.bounced, ou email.complained événements sur localhost.
Ce qu'un webhook Resend indique à votre application
Une réponse API indiquant qu'un e-mail a été accepté ne constitue pas une preuve qu'il est parvenu au destinataire. La livraison s'effectue de manière asynchrone. Les webhooks Resend permettent à votre application de mettre à jour l'état des messages, de supprimer les mauvaises adresses, de signaler les rebonds et de réagir aux plaintes une fois la demande d'envoi d'origine terminée. Le documentation officielle du webhook Resend répertorie les types d’événements et la configuration du tableau de bord.
Un test de webhook local doit couvrir l'intégralité de la machine à états, et pas seulement si un POST atteint votre itinéraire. Corrélez l'identifiant de messagerie de chaque événement avec l'enregistrement créé lors de l'envoi. Traitez les états comme des transitions : accepté, envoyé, livré, retardé, rejeté, réclamé, ouvert ou cliqué, le cas échéant. Une copie ultérieure ne doit pas écraser un état plus utile ni déclencher deux fois la même alerte.
Créer l'itinéraire Next.js App Router
Installez le vérificateur géré pour le format de signature :
npm install svix
Créez ensuite une route d'exécution de nœud. Resend signe le corps d'origine, utilisez donc request.text() exactement une fois avant l'analyse.
// 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 });
}
Le secret de signature appartient à ce point de terminaison webhook et commence normalement par un préfixe spécifique au fournisseur. Copiez-le depuis les paramètres du webhook Resend dans un fichier d'environnement local ignoré tel que .env.local. Il ne s'agit pas de la clé API Resend utilisée pour envoyer des e-mails.
Pourquoi les trois en-têtes Svix sont importants
svix-ididentifie de manière unique une livraison et constitue la meilleure clé d'idempotence.svix-timestamplie la signature à une heure, permettant au vérificateur de rejeter les demandes périmées en dehors de sa tolérance.svix-signaturepeut contenir une ou plusieurs signatures versionnées utilisées pour authentifier le corps.
N'implémentez pas ce protocole en divisant les chaînes d'en-tête, sauf si vous avez une raison impérieuse. Le SDK gère le codage, les signatures multiples et les vérifications d'horodatage. Resend recommande explicitement d'utiliser le secret de signature et ces en-têtes pour la vérification. Plus profond guide de vérification de signature explique pourquoi les octets bruts et les contrôles de synchronisation sont importants.
Exposez Next.js et enregistrez le point de terminaison
- Courir
npm run devet confirmez que l'application écoute sur le port 3000. - Commencer
npx portpreview 3000dans un autre terminal. - Dans Resend, créez un webhook dont le point de terminaison est
https://YOUR-TUNNEL.portpreview.dev/api/webhooks/resend. - Sélectionnez uniquement les événements de courrier électronique traités par votre candidature.
- Copiez le secret de signature du point de terminaison dans
RESEND_WEBHOOK_SECRETet redémarrez Next.js pour qu'il charge la variable. - Envoyez un message en utilisant un domaine vérifié et inspectez les événements qui atteignent la route locale.
Gardez l'URL publique stable pour la session. Si l'origine du tunnel change, modifiez le point de terminaison Resend avant de tester à nouveau. Un point de terminaison configuré avec une ancienne URL ne peut pas atteindre votre nouveau processus, même si localhost lui-même est sain.
Utiliser un répartiteur d'événements typé
Les charges utiles des webhooks doivent entrer dans un répartiteur restreint. Validez les champs obligatoires et rendez observables les types d’événements non reconnus sans les traiter comme des pannes de serveur.
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);
}
}
Gardez les types de charge utile alignés sur le schéma Resend actuel plutôt que de supposer que chaque événement a des données identiques. Par exemple, les détails des rebonds et les listes de destinataires peuvent n'être pertinents que pour certains événements. Enregistrez le type d'événement, l'ID de messagerie du fournisseur, l'horodatage de l'événement et une charge utile minimale rédigée pour les enquêtes d'assistance.
Rendre la gestion idempotente avant les nouvelles tentatives de test
Dans la pratique, les systèmes Webhook offrent un comportement de livraison au moins une fois. Un délai d'attente peut survenir après la validation de votre base de données mais avant que le fournisseur ne reçoive votre réponse 200. Le fournisseur réessaye ensuite une demande que vous avez déjà appliquée. Utiliser svix-id comme clé de livraison unique et insérez-la dans la même transaction que le changement d'état.
await db.transaction(async (tx) => {
const inserted = await tx.webhookDelivery.insertOnce({
provider: 'resend',
deliveryId,
});
if (!inserted) return;
await applyEmailEvent(tx, event);
});
Ne effectuez pas de déduplication uniquement par identifiant de messagerie, car un e-mail reçoit légitimement plusieurs types d'événements. En fonction de votre modèle de données, conservez à la fois une clé unique au niveau de la livraison et des règles de transition d'état. Lire modèles de tentatives de webhook et d'idempotence avant de connecter les événements à la facturation, à la suppression ou aux notifications client.
Revenez rapidement sans perdre l'événement
La vérification de la signature est appropriée dans le chemin de la demande ; le travail lent ne l’est pas. Conservez ou mettez en file d’attente l’événement vérifié, puis renvoyez 2xx. Si vous revenez avant toute écriture durable, un crash de processus peut perdre l'événement. Si vous attendez plusieurs API distantes, votre point de terminaison peut expirer et inviter à de nouvelles tentatives. Une table de boîte de réception de base de données est souvent la conception locale et de production la plus simple.
Générer des événements de test utiles
Envoyé et livré
Envoyez à une adresse que vous contrôlez à partir d'un domaine vérifié. Enregistrez l'ID de messagerie renvoyé par l'API d'envoi et confirmez que les événements entrants mettent à jour la même ligne. Le délai de livraison varie selon le serveur destinataire. Ne présumez donc pas que les événements arrivent immédiatement ou dans une séquence simpliste.
Rebonds
Utilisez les adresses de test documentées ou les fonctionnalités de test de Resend plutôt que d'inventer du trafic vers des domaines non liés. Vérifiez que les échecs permanents suppriment les futurs messages tandis que les conditions temporaires suivent votre politique de nouvelle tentative. Ne supprimez pas automatiquement chaque événement retardé.
Plaintes
Le traitement des réclamations relève à la fois d’une logique de délivrabilité et de conformité. Assurez-vous qu'un webhook répété ne crée pas d'alertes répétées et assurez-vous que le destinataire concerné est exclu des campagnes ultérieures conformément à votre politique.
Résoudre les échecs du webhook Resend
La vérification de la signature échoue toujours
Vérifiez que le secret de signature du point de terminaison (et non la clé API) est chargé. Utiliser await request.text(), n'analysez pas et ne re-stringifiez pas JSON, et transmettez les trois en-têtes Svix avec leurs valeurs exactes. Redémarrez le serveur de développement après avoir modifié .env.local.
L'itinéraire renvoie 404 ou 405
Les fichiers de route App Router doivent être nommés route.ts sous les segments d'URL prévus et exporter POST. Vérifiez si le middleware réécrit la demande de tunnel dans une page de paramètres régionaux ou de connexion. Testez l'URL publique avec curl et inspectez la réponse réelle.
Resend affiche les tentatives malgré un traitement réussi
Vérifiez que chaque branche réussie renvoie rapidement 2xx. Les erreurs générées après une mise à jour de la base de données peuvent produire une tentative 500 et une nouvelle tentative en double. Rendez le traitement transactionnel et idempotent, puis inspectez la latence de réponse.
Les événements arrivent mais ne peuvent pas être liés à un e-mail
Conservez l’ID de messagerie du fournisseur de la réponse d’envoi Resend d’origine. Ne vous fiez pas aux lignes d'objet ou aux adresses des destinataires comme identifiants. Ces champs ne sont ni suffisamment uniques ni suffisamment stables pour une corrélation.
Les captures rejouées échouent à la vérification de l'horodatage
Cela est normal lors de la relecture d'une ancienne requête signée via le vérificateur normal : son horodatage peut être en dehors de la tolérance autorisée. Préférez la relivraison par un prestataire lorsqu'elle est disponible. Pour les tests de logique métier isolés, vérifiez une fois, enregistrez un événement vérifié et testez le répartiteur séparément. Le guide de relecture explique cette frontière.
Sécurité et confidentialité pour les tests d'événements de courrier électronique
- Ne jamais exposer
RESEND_API_KEYou le secret de signature du point de terminaison dans la source, les bundles de navigateur, les captures d'écran ou les journaux de requêtes. - Vérifiez avant d’analyser ou de conserver l’événement.
- Supprimez les destinataires, les sujets, les en-têtes et les métadonnées des messages à partir des captures de tunnel partagé.
- Appliquer des limites de conservation aux charges utiles brutes des webhooks ; stockez uniquement ce qu’exigent le support et la conformité.
- Utilisez un secret de point de terminaison local distinct de la production et faites-le pivoter lorsque le point de terminaison de test est supprimé.
La conception finale devrait fonctionner de manière identique après le déploiement : point de terminaison HTTPS public, vérification du corps brut, idempotence durable, accusé de réception rapide et gestion de l'état asynchrone. Pour plus de détails sur le corps brut spécifique à App Router, consultez le Guide de l'hôte local du webhook Next.js.
