Alle Artikel
Postmark-Webhooks lokal über HTTPS testen
Postmarkemail webhookslocalhostwebhook security

Postmark-Webhooks lokal über HTTPS testen

Um Postmark-Webhooks auf localhost zu testen, starten Sie den lokalen Handler, geben den Port mit npx portpreview PORT frei und hinterlegen die HTTPS-Route beim passenden Message Stream. Schützen Sie sie mit Basic Authentication oder einem geheimen Header, validieren Sie JSON, speichern Sie idempotent und antworten Sie zügig mit HTTP 200.

Was Postmark an einen Webhook sendet

Postmark sendet bei E-Mail-Ereignissen HTTP POST. Outbound Message Streams melden Delivery, Bounce, Open, Click, Spam Complaint und Subscription Change; Inbound Streams senden geparste Eingangspost. Nach RecordType routen und je Typ validieren. Delivery belegt nur die Annahme durch den Zielserver, nicht den Posteingang. Bounce liefert unter anderem Type, TypeCode, Inactive und CanActivate. offizielle Webhook-Übersicht Bounce-Webhook-Referenz

Einen kleinen lokalen Express-Empfänger bauen

Das Express-Beispiel auf Port 3000 prüft Basic Auth vor JSON, validiert die Mindesthülle und schreibt einen Deduplizierungsschlüssel dauerhaft, bevor es bestätigt. Ersetzen Sie die Helfer durch eine Transaktion oder dauerhafte Queue. Begrenzen Sie Requests und protokollieren Sie keine kompletten Eingangsmails mit personenbezogenen Daten, Login-Links oder Anhängen.

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

localhost über eine öffentliche HTTPS-URL erreichbar machen

Empfänger lokal prüfen, dann in einem zweiten Terminal npx portpreview 3000 ausführen und die Route wie https://YOUR-TUNNEL.portpreview.dev/webhooks/postmark ergänzen. Der Tunnel liefert Erreichbarkeit und HTTPS, authentifiziert aber Postmark nicht. Anwendungsauthentifizierung und Payload-Validierung bleiben erforderlich. Sicherheitsleitfaden für localhost-Tunnel

Den richtigen Postmark-Webhook konfigurieren

Für Outbound im richtigen Server und Message Stream die URL unter Webhooks eintragen und nur unterstützte Trigger aktivieren. Inbound besitzt eine eigene URL. Die Webhooks API unterstützt HttpAuth, HttpHeaders und Trigger. X-Postmark-Server-Token dient API-Aufrufen und wird nicht als Empfänger-Credential gesendet. Webhooks API

Postmark-Authentifizierung ist keine kryptografische Signatur

Postmark unterstützt derzeit keine HMAC-Webhook-Signaturen. Es gibt weder einen Signing Secret zum Hashen des Raw Body noch X-Postmark-Signature. Basic Authentication, IP-Allowlist und geheime Header weisen ein gemeinsames Credential nach, binden es aber nicht kryptografisch an den Body. HTTPS und Payload-Validierung sind nötig; IP-Bereiche müssen aktuell sein. Bevorzugen Sie HttpAuth. Bei https://username:[email protected]/path sind eigene starke, korrekt codierte Zugangsdaten nötig. Server API Tokens niemals wiederverwenden.

Zustellungs- und Bounce-Ereignisse nach Typ verarbeiten

Geschäftslogik gehört aus dem HTTP-Pfad in einen Worker, der gespeicherte Ereignisse idempotent verarbeitet. Keine dauerhafte Sperre allein aus einem Feldnamen ableiten; aktuelle Bounce-Klassifizierung und Versandrichtlinie verwenden. Spam Complaint und Subscription Change sind eigene Typen, Open und Click können mehrfach eintreffen.

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

Wiederholungen und doppelte Zustellung einplanen

Ohne HTTP 200 wiederholt Postmark. Bounce und Inbound laufen länger als Click, Open, Delivery und Subscription Change; 403 beendet Versuche. Ein Timeout nach dem Commit erzeugt ein legitimes Duplikat. Einen stabilen Schlüssel mit Unique Constraint aus MessageID, bei gemischten Endpoints zusätzlich RecordType und etwa ID, verwenden. Erst nach minimaler dauerhafter Übergabe 200 senden und externe APIs asynchron bearbeiten. Webhook-Wiederholungen und Idempotenz

Echte Ereignisse sicher testen

Zuerst einen synthetischen curl-POST prüfen, danach Delivery an eine eigene Adresse und Bounce mit dokumentierten Postmark-Testmitteln einschließlich black-hole test domain, sofern verfügbar. MessageID speichern, ein bereinigtes Fixture zweimal senden und genau einen Seiteneffekt erwarten.

Häufige Postmark-Webhook-Fehler beheben

Der Endpoint ist nicht erreichbar

Tunnel, vollständige Route und Port prüfen. Bei 401 Credentials vergleichen, App neu starten und ohne Wertausgabe prüfen, ob ein Proxy Authorization entfernt. Bei Wiederholungen öffentlichen Status und Latenz kontrollieren; Postmark verlangt 200. Bei Payload-Abweichungen RecordType, Trigger und Inbound/Outbound abgleichen, fehlende Pflichtfelder ablehnen und dokumentierte Ergänzungen tolerieren.

Alle Requests liefern HTTP 401

Webhook-Leitfaden zu 401/403

Wiederholungen trotz Verarbeitung

Tunnel, vollständige Route und Port prüfen. Bei 401 Credentials vergleichen, App neu starten und ohne Wertausgabe prüfen, ob ein Proxy Authorization entfernt. Bei Wiederholungen öffentlichen Status und Latenz kontrollieren; Postmark verlangt 200. Bei Payload-Abweichungen RecordType, Trigger und Inbound/Outbound abgleichen, fehlende Pflichtfelder ablehnen und dokumentierte Ergänzungen tolerieren.

Das Payload stimmt nicht überein

Tunnel, vollständige Route und Port prüfen. Bei 401 Credentials vergleichen, App neu starten und ohne Wertausgabe prüfen, ob ein Proxy Authorization entfernt. Bei Wiederholungen öffentlichen Status und Latenz kontrollieren; Postmark verlangt 200. Bei Payload-Abweichungen RecordType, Trigger und Inbound/Outbound abgleichen, fehlende Pflichtfelder ablehnen und dokumentierte Ergänzungen tolerieren.

Sicherheitscheckliste für den Produktivbetrieb

HTTPS und eigene starke Basic-Auth- oder Header-Credentials verwenden; API-Tokens trennen; nach Tests rotieren und alte URLs löschen; Content Type, Größe, Typ und IDs validieren; Maildaten und Secrets in Logs schwärzen; Least Privilege anwenden sowie Fehler, Verzögerung, Duplikate und Dead Letters überwachen.

Leitfaden zur lokalen Webhook-Fehlersuche

Häufig gestellte Fragen

Wie teste ich einen Postmark-Webhook auf localhost?
Lokalen Handler starten, Port mit npx portpreview PORT freigeben, Route an die HTTPS-URL hängen und im richtigen Message Stream eintragen.
Signiert Postmark Webhooks mit HMAC?
Nein. Aktuell gibt es keine HMAC-Signaturen. HTTPS mit Basic Authentication, optional aktuelle IP-Allowlist, und Payload-Validierung verwenden.
Warum sendet Postmark denselben Webhook erneut?
Ohne HTTP 200 wird wiederholt. Ein stabiler Ereignisschlüssel mit Unique Constraint verhindert doppelte Wirkung.
Bedeutet Delivery, dass die E-Mail gelesen wurde?
Nein. Nur der Ziel-Mailserver hat sie angenommen; Posteingang, Öffnung oder Lesen sind damit nicht belegt.