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
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.
