Tous les articles
Tester les webhooks Postmark en local avec HTTPS
Postmarkemail webhookslocalhostwebhook security

Tester les webhooks Postmark en local avec HTTPS

Pour tester un webhook Postmark sur localhost, lancez votre gestionnaire en local, exposez son port avec npx portpreview PORT, puis enregistrez la route HTTPS obtenue sur le Message Stream Postmark concerné. Protégez la route par Basic Authentication ou par un en-tête secret personnalisé, validez chaque payload JSON, enregistrez-le de façon idempotente et renvoyez rapidement HTTP 200.

Ce que Postmark envoie à un webhook

Lorsqu'un événement d'e-mail survient, Postmark effectue une requête HTTP POST, ce qui évite d'interroger régulièrement son API. Les Message Streams sortants peuvent signaler une livraison, un rebond, une ouverture, un clic, une plainte pour spam ou un changement d'abonnement. Les Message Streams entrants publient l'e-mail reçu et analysé vers leur URL entrante. Les champs varient selon l'événement : utilisez RecordType pour l'aiguillage et validez le schéma propre à ce type.

Une livraison signifie que le serveur de messagerie destinataire a accepté le message, pas qu'il est arrivé dans la boîte de réception. Un rebond décrit un échec et fournit notamment Type, TypeCode, Inactive et CanActivate. La présentation officielle des webhooks Postmark détaille la protection et les nouvelles tentatives ; la référence du webhook de rebond documente les champs.

Créer un petit récepteur Express local

Cet exemple utilise Express sur le port 3000. Il contrôle Basic Auth avant d'accepter le JSON, valide l'enveloppe minimale, enregistre durablement une clé de déduplication, puis confirme seulement après cette écriture. Remplacez les fonctions de base de données par votre transaction ou votre file.

import crypto from 'node:crypto';
import express from 'express';

const app = express();
app.use('/webhooks/postmark', express.json({ limit: '2mb' }));

function safeEqual(actual, expected) {
  const a = Buffer.from(actual);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function authorized(req) {
  const value = req.get('authorization') ?? '';
  if (!value.startsWith('Basic ')) return false;
  const decoded = Buffer.from(value.slice(6), 'base64').toString('utf8');
  const separator = decoded.indexOf(':');
  if (separator < 0) return false;
  return safeEqual(decoded.slice(0, separator), process.env.POSTMARK_WEBHOOK_USER ?? '') &&
    safeEqual(decoded.slice(separator + 1), process.env.POSTMARK_WEBHOOK_PASSWORD ?? '');
}

app.post('/webhooks/postmark', async (req, res) => {
  if (!authorized(req)) return res.sendStatus(401);

  const event = req.body;
  if (typeof event?.RecordType !== 'string' ||
      typeof event?.MessageID !== 'string') {
    return res.status(400).json({ error: 'Invalid Postmark event' });
  }

  const deliveryKey = `${event.RecordType}:${event.MessageID}:${event.ID ?? ''}`;
  await saveWebhookOnce({ provider: 'postmark', deliveryKey, event });
  res.sendStatus(200);
});

app.listen(3000);

Pour les actions sensibles, appliquez une liste stricte de champs autorisés. Limitez la taille des requêtes, surtout pour les webhooks entrants ou les rebonds incluant le contenu du message. Ne journalisez pas les messages entrants complets : ils peuvent contenir des données personnelles, des liens d'authentification, des pièces jointes ou des échanges confidentiels.

Exposer localhost avec une URL HTTPS publique

  1. Démarrez le récepteur et vérifiez sa réponse locale sur le port prévu.
  2. Dans un second terminal, exécutez npx portpreview 3000, en remplaçant le port si nécessaire.
  3. Ajoutez la route à l'origine HTTPS générée, par exemple https://YOUR-TUNNEL.portpreview.dev/webhooks/postmark.
  4. Laissez le tunnel actif pendant les envois de Postmark. Si l'origine publique change, mettez à jour sa configuration.

Le tunnel apporte l'accessibilité publique et un HTTPS de confiance ; il n'authentifie pas Postmark. Toute personne découvrant l'URL peut envoyer un POST : l'authentification applicative et la validation restent indispensables. Consultez le guide de sécurité des tunnels localhost avant d'exposer un gestionnaire relié à des données clients réelles.

Configurer le bon webhook Postmark

Pour les livraisons, rebonds, ouvertures, clics, plaintes pour spam ou changements d'abonnement, connectez-vous à Postmark, choisissez le Server et le Message Stream sortant, ouvrez Webhooks, ajoutez l'URL publique et n'activez que les déclencheurs gérés. Les libellés peuvent évoluer : vérifiez toujours le Message Stream sélectionné. Pour les e-mails entrants, configurez le webhook dans les paramètres du Inbound Message Stream, qui possède sa propre URL entrante.

Pour provisionner les hooks sous forme de code, utilisez l'API Webhooks officielle. L'objet accepte les identifiants HttpAuth, des HttpHeaders facultatifs et les déclencheurs. Les appels de gestion emploient X-Postmark-Server-Token : ce jeton appelle l'API Postmark et n'est pas un identifiant envoyé par Postmark à votre récepteur.

L'authentification Postmark n'est pas une signature cryptographique

La documentation actuelle indique que Postmark ne prend pas en charge la vérification de signature HMAC des webhooks. Il n'existe donc pas de secret de signature Postmark permettant de recalculer une empreinte du corps brut. Ne cherchez pas d'en-tête X-Postmark-Signature et ne copiez pas le vérificateur d'un autre fournisseur.

Postmark documente HTTP Basic Authentication et la liste blanche d'adresses IP ; l'API permet aussi des en-têtes HTTP personnalisés. Basic Auth ou un en-tête secret prouve la possession d'un secret partagé, et HTTPS le protège en transit, mais aucun ne lie cryptographiquement ce secret au corps exact. Validez ensuite la forme et les valeurs du payload. Pour un filtrage IP en défense supplémentaire, utilisez les plages publiées à jour : l'adresse source peut changer entre deux tentatives.

Préférez les champs HttpAuth de l'API aux identifiants intégrés dans une URL susceptible d'apparaître dans des journaux ou captures. Si vous utilisez la forme documentée https://username:[email protected]/path, créez des identifiants dédiés et très aléatoires, encodez les caractères réservés et évitez toute divulgation. Ne réutilisez jamais un jeton d'API Server Postmark comme mot de passe du webhook.

Traiter les événements de livraison et de rebond selon leur type

Sortez la logique métier du chemin HTTP. Un worker peut distribuer les événements persistés et rendre chaque transition idempotente :

async function processPostmarkEvent(event) {
  switch (event.RecordType) {
    case 'Delivery':
      await markAcceptedByRecipientServer({
        messageId: event.MessageID,
        deliveredAt: event.DeliveredAt
      });
      break;
    case 'Bounce':
      await recordBounce({
        bounceId: String(event.ID),
        messageId: event.MessageID,
        type: event.Type,
        inactive: event.Inactive,
        canActivate: event.CanActivate
      });
      break;
    default:
      await recordUnhandledPostmarkType(event.RecordType);
  }
}

Ne déduisez pas une exclusion définitive du seul nom d'un champ : suivez la classification de rebond actuelle et votre politique d'envoi. Plaintes pour spam et changements d'abonnement ont leurs propres types. Ouvertures et clics peuvent se répéter ; choisissez entre conserver chaque occurrence et agréger.

Prévoir les nouvelles tentatives et les doublons

Postmark recommence s'il ne reçoit pas HTTP 200. Le calendrier varie : rebonds et messages entrants ont une séquence plus longue que clics, ouvertures, livraisons et changements d'abonnement. Une réponse 403 arrête les tentatives. Vérifiez la documentation actuelle avant de dépendre d'intervalles précis.

Un timeout peut survenir après le commit en base mais avant la réception de la réponse par Postmark : le POST répété est alors légitime. Placez une contrainte unique sur une clé stable et enregistrez l'événement dans la transaction qui le revendique. Postmark recommande MessageID ; sur un endpoint mixte, ajoutez RecordType et, si disponible, un identifiant propre à l'événement comme ID pour un rebond.

Renvoyez 200 seulement après un transfert durable minimal. Répondre avant un travail de fond non suivi risque une perte ; attendre les API d'e-mail, CRM et analytics augmente latence et doublons. Une boîte de réception en base ou une file durable est le bon compromis. Voir les modèles de nouvelles tentatives et d'idempotence.

Tester sans risque le traitement réel des événements

Envoyez d'abord un POST synthétique avec curl pour vérifier routage, authentification, validation et persistance, puis produisez des événements réels. Une adresse que vous contrôlez convient à la livraison. Pour les rebonds, utilisez les dispositifs de test documentés par Postmark, dont son domaine « black hole » le cas échéant, plutôt que des adresses inventées. Vérifiez la compatibilité de votre compte et du Message Stream.

Conservez le MessageID renvoyé lors de l'envoi et corrélez-le aux webhooks. Publiez deux fois le même fixture nettoyé pour confirmer que les effets n'arrivent qu'une fois. Ne simulez une panne transitoire que dans un environnement contrôlé et vérifiez que la relance n'entraîne ni seconde notification ni seconde exclusion.

Diagnostiquer les pannes fréquentes des webhooks Postmark

Aucune requête n'atteint localhost

Vérifiez que le tunnel tourne, que l'URL contient la route complète et que le serveur écoute le port donné à PortPreview. Testez vous-même l'URL publique : une page d'accueil du tunnel confirme seulement l'accès à l'edge, pas l'existence de la route POST.

Toutes les requêtes renvoient 401

Comparez utilisateur et mot de passe configurés avec l'environnement local, redémarrez après leur modification et vérifiez qu'un reverse proxy ne retire pas Authorization. N'affichez jamais sa valeur. Le guide de diagnostic webhook 401/403 propose une procédure sûre.

Postmark recommence alors que le traitement a réussi

Contrôlez le statut et la latence réellement observés sur l'endpoint public. Postmark attend précisément 200. Empêchez qu'une erreur après l'écriture transforme la réponse en 500 et rendez l'écriture idempotente avant de provoquer une nouvelle livraison.

Le payload ne correspond pas à l'exemple

Vérifiez RecordType, le déclencheur et le caractère entrant ou sortant du stream. Les schémas peuvent gagner des champs : refusez les champs obligatoires absents, acceptez les ajouts facultatifs documentés et synchronisez les fixtures avec la référence officielle du type.

Liste de contrôle de sécurité en production

  • Utilisez HTTPS et des identifiants Basic Auth ou d'en-tête personnalisé dédiés et très aléatoires.
  • Séparez jetons API Server, identifiants de webhook et secrets de production.
  • Renouvelez les identifiants après les tests et supprimez les anciennes URL.
  • Validez type de contenu, taille, type d'événement, identifiants et champs requis.
  • Masquez dans les logs adresses, objets, contenu, identifiants et métadonnées.
  • Appliquez le moindre privilège au worker et aux données.
  • Surveillez échecs d'authentification, retard, doublons et dead letters.

Un bon test local reproduit la production : entrée HTTPS authentifiée, validation stricte, idempotence durable, réponse HTTP 200 rapide et logique métier asynchrone. Pour aller plus loin, consultez le guide de débogage local des webhooks.

Questions fréquentes

Comment tester un webhook Postmark sur localhost ?
Lancez le gestionnaire local, exposez son port avec npx portpreview PORT, ajoutez la route à l'URL HTTPS générée et configurez cette URL sur le Message Stream Postmark concerné.
Postmark signe-t-il les requêtes webhook avec HMAC ?
Non. La documentation actuelle de Postmark ne prend pas en charge les signatures HMAC. Utilisez HTTPS avec Basic Authentication et, éventuellement, la liste d'IP Postmark à jour, puis validez chaque payload.
Pourquoi Postmark renvoie-t-il le même webhook ?
Postmark recommence s'il ne reçoit pas HTTP 200 ; un timeout ou une réponse en échec peut donc répéter un événement déjà traité. Enregistrez une clé stable sous contrainte unique.
Un webhook de livraison Postmark signifie-t-il que l'e-mail a été lu ?
Non. La livraison indique seulement que le serveur destinataire a accepté l'e-mail ; elle ne prouve ni son placement en boîte de réception ni son ouverture.