Запустіть локальний handler, відкрийте порт через npx portpreview PORT і вкажіть HTTPS-маршрут у потрібному Message Stream. Захистіть Basic Authentication або секретним header, перевіряйте JSON, зберігайте ідемпотентно та швидко відповідайте HTTP 200.
Що Postmark надсилає у вебхук
Postmark надсилає HTTP POST для Delivery, Bounce, Open, Click, Spam Complaint, Subscription Change та вхідної пошти. Розподіляйте за RecordType. Delivery означає приймання сервером, але не inbox. Bounce містить Type, TypeCode, Inactive, CanActivate. офіційний огляд вебхуків довідник bounce-вебхука
Невеликий локальний приймач Express
Express на порту 3000 спершу перевіряє Basic Auth, потім мінімальну схему та надійно записує ключ дедуплікації. Замініть helpers транзакцією або чергою. Обмежте розмір і не логуйте повні листи з персональними даними, посиланнями та вкладеннями.
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);
Публічна HTTPS-адреса для localhost
Перевірте локальний порт, виконайте npx portpreview 3000 і додайте маршрут https://YOUR-TUNNEL.portpreview.dev/webhooks/postmark. Тунель дає HTTPS і доступність, але не автентифікує Postmark. посібник із безпеки localhost-тунелів
Налаштування правильного вебхука Postmark
Для outbound оберіть правильні Server і Message Stream, додайте URL і потрібні triggers. Inbound Message Stream має власний URL. API підтримує HttpAuth, HttpHeaders; X-Postmark-Server-Token потрібен для API й не надсилається приймачу. Webhooks API
Автентифікація Postmark — не криптографічний підпис
Postmark не підтримує HMAC-підпис вебхуків: немає signing secret і X-Postmark-Signature. Basic Authentication, IP allowlist і секретний header підтверджують спільний секрет, але не пов’язують його криптографічно з body. Використовуйте HTTPS, валідацію та актуальні IP. Надавайте перевагу HttpAuth; для https://username:[email protected]/path потрібні окремі стійкі, правильно закодовані дані. Не використовуйте Server API token.
Обробка delivery і bounce за типом
Винесіть бізнес-логіку у worker та обробляйте збережені події ідемпотентно. Не встановлюйте постійне блокування за одним полем; враховуйте актуальну класифікацію Bounce. Spam Complaint і Subscription Change — окремі типи, Open і Click повторюються.
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);
}
}
Проєктування з урахуванням повторів і дублів
Без HTTP 200 Postmark повторює запит; Bounce і Inbound довше за Click, Open, Delivery і Subscription Change, а 403 зупиняє повтори. Дубль після commit і timeout є легітимним. Створіть unique key із MessageID, а для змішаного endpoint додайте RecordType та ID. Відповідайте 200 після надійної передачі, зовнішні API викликайте асинхронно. повтори та ідемпотентність вебхуків
Безпечна перевірка справжніх подій
Спершу надішліть curl POST, потім лист на власну адресу для Delivery. Bounce перевіряйте офіційними засобами Postmark, зокрема black-hole test domain, якщо доступний. Збережіть MessageID і двічі надішліть очищений fixture, очікуючи один ефект.
Діагностика типових збоїв вебхука Postmark
Запит не доходить до endpoint
Перевірте тунель, маршрут і порт. Для 401 звірте credentials, перезапустіть застосунок і перевірте, чи proxy не видаляє Authorization, не виводячи значення. Для повторів перевірте публічні status і latency: потрібен 200. Для іншого payload звірте RecordType, trigger і stream.
Усі запити повертають HTTP 401
Повтори після успішної обробки
Перевірте тунель, маршрут і порт. Для 401 звірте credentials, перезапустіть застосунок і перевірте, чи proxy не видаляє Authorization, не виводячи значення. Для повторів перевірте публічні status і latency: потрібен 200. Для іншого payload звірте RecordType, trigger і stream.
Payload не збігається зі зразком
Перевірте тунель, маршрут і порт. Для 401 звірте credentials, перезапустіть застосунок і перевірте, чи proxy не видаляє Authorization, не виводячи значення. Для повторів перевірте публічні status і latency: потрібен 200. Для іншого payload звірте RecordType, trigger і stream.
Чекліст безпеки для production
Використовуйте HTTPS і окремі стійкі credentials; розділяйте API tokens; робіть ротацію після тесту; видаляйте старі URL; перевіряйте тип, розмір і поля; приховуйте поштові дані та секрети; застосовуйте least privilege; стежте за помилками, lag, дублями й dead letters.
