Все статьи
Как тестировать вебхуки Postmark локально через HTTPS
Postmarkemail webhookslocalhostwebhook security

Как тестировать вебхуки Postmark локально через HTTPS

Запустите локальный обработчик, откройте порт через npx portpreview PORT и укажите HTTPS-маршрут в нужном Message Stream. Защитите его Basic Authentication или секретным заголовком, проверяйте JSON, сохраняйте идемпотентно и быстро отвечайте HTTP 200.

Что Postmark отправляет в вебхук

Postmark отправляет HTTP POST для Delivery, Bounce, Open, Click, Spam Complaint, Subscription Change и входящей почты. Маршрутизируйте по RecordType. Delivery означает прием сервером, но не попадание во входящие. 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.

Обработка доставки и 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/403

Повторы после успешной обработки

Проверьте туннель, маршрут и порт. При 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.

руководство по локальной отладке вебхуков

Часто задаваемые вопросы

Как проверить вебхук Postmark на localhost?
Запустите handler, откройте порт через npx portpreview PORT, добавьте маршрут к HTTPS URL и настройте его в нужном Message Stream.
Подписывает ли Postmark вебхуки через HMAC?
Нет. HMAC-подписи не поддерживаются. Используйте HTTPS с Basic Authentication, при необходимости актуальный IP allowlist, и проверяйте payload.
Почему Postmark повторяет вебхук?
Если нет HTTP 200, запрос повторяется. Сохраняйте стабильный ключ под unique constraint.
Означает ли Delivery, что письмо прочитано?
Нет. Это лишь прием сервером назначения, а не гарантия inbox, открытия или прочтения.