Chạy handler cục bộ, mở cổng bằng npx portpreview PORT, rồi đăng ký route HTTPS với đúng Message Stream. Dùng Basic Authentication hoặc header bí mật, kiểm tra JSON, lưu idempotent và nhanh chóng trả HTTP 200.
Postmark gửi gì tới webhook
Postmark gửi HTTP POST cho các sự kiện Delivery, Bounce, Open, Click, Spam Complaint, Subscription Change và thư đến. Định tuyến theo RecordType. Delivery chỉ là máy chủ đích chấp nhận thư; Bounce có Type, TypeCode, Inactive, CanActivate. tổng quan webhook chính thức tài liệu bounce webhook
Tạo bộ nhận Express nhỏ trên máy
Ví dụ Express cổng 3000 kiểm tra Basic Auth, schema tối thiểu và lưu bền vững khóa chống trùng trước khi phản hồi. Thay helper bằng transaction hoặc queue; giới hạn kích thước và không log toàn bộ email nhạy cảm.
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);
Công khai localhost bằng URL HTTPS
Kiểm tra cổng rồi chạy npx portpreview 3000, thêm route https://YOUR-TUNNEL.portpreview.dev/webhooks/postmark. Tunnel cung cấp HTTPS nhưng không xác thực Postmark. hướng dẫn bảo mật đường hầm localhost
Cấu hình đúng Postmark webhook
Chọn đúng Server và Message Stream, chỉ bật trigger đã hỗ trợ; inbound có URL riêng. API có HttpAuth, HttpHeaders; X-Postmark-Server-Token chỉ gọi API. Webhooks API
Xác thực Postmark không phải chữ ký mật mã
Postmark không hỗ trợ chữ ký HMAC webhook, không có signing secret hay X-Postmark-Signature. Basic Authentication, IP allowlist hoặc secret header không ràng buộc body bằng mật mã. Dùng HTTPS, validation, IP hiện hành và HttpAuth; không tái dùng Server API token.
Xử lý delivery và bounce theo loại
Đưa nghiệp vụ sang worker idempotent. Dùng phân loại Bounce hiện hành; Spam Complaint và Subscription Change là loại riêng, Open và Click có thể lặp.
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);
}
}
Thiết kế cho retry và bản giao trùng
Không có HTTP 200 thì Postmark retry; 403 dừng retry. Dùng unique key từ MessageID, thêm RecordType và ID khi cần. Trả 200 sau durable handoff và gọi API ngoài bất đồng bộ. retry và idempotency cho webhook
Kiểm thử sự kiện thật an toàn
Gửi curl POST trước, rồi thử Delivery tới địa chỉ của bạn và Bounce bằng công cụ Postmark như black-hole test domain nếu hỗ trợ. Lưu MessageID và gửi fixture sạch hai lần để kiểm tra idempotency.
Khắc phục lỗi Postmark webhook thường gặp
Request không tới endpoint
Kiểm tra tunnel, route, port. Với 401, kiểm tra credential và proxy không xóa Authorization. Với retry, kiểm tra status 200 và latency. So khớp RecordType, trigger và stream nếu payload khác.
Mọi request đều trả HTTP 401
Vẫn retry sau khi xử lý
Kiểm tra tunnel, route, port. Với 401, kiểm tra credential và proxy không xóa Authorization. Với retry, kiểm tra status 200 và latency. So khớp RecordType, trigger và stream nếu payload khác.
Payload không khớp mẫu
Kiểm tra tunnel, route, port. Với 401, kiểm tra credential và proxy không xóa Authorization. Với retry, kiểm tra status 200 và latency. So khớp RecordType, trigger và stream nếu payload khác.
Danh sách bảo mật production
Dùng HTTPS và credential riêng mạnh; tách API token; rotate sau test; xóa URL cũ; validate loại, kích thước và trường; che dữ liệu trong log; dùng least privilege; theo dõi lỗi, lag, duplicate và dead letter.
