Todos os artigos
Testar webhooks da Postmark localmente com HTTPS
Postmarkemail webhookslocalhostwebhook security

Testar webhooks da Postmark localmente com HTTPS

Execute o handler local, exponha a porta com npx portpreview PORT e cadastre a rota HTTPS no Message Stream correto. Proteja com Basic Authentication ou header secreto, valide o JSON, salve de forma idempotente e responda HTTP 200 rapidamente.

O que a Postmark envia ao webhook

A Postmark faz um HTTP POST quando ocorre um evento. Outbound Message Streams informam Delivery, Bounce, Open, Click, Spam Complaint e Subscription Change; Inbound Message Streams enviam o e-mail recebido já analisado. Direcione pelo RecordType e valide o schema de cada tipo. Delivery só confirma que o servidor de destino aceitou a mensagem, não que ela chegou à caixa de entrada. Bounce inclui Type, TypeCode, Inactive e CanActivate. visão geral oficial de webhooks referência do webhook de bounce

Criar um pequeno receptor Express local

O exemplo usa Express na porta 3000, verifica Basic Auth antes do JSON, valida o envelope mínimo e grava uma chave de deduplicação de forma durável antes de confirmar. Troque os helpers por sua transação ou fila durável. Limite o tamanho da requisição e não registre e-mails inteiros, que podem conter dados pessoais, links de acesso, anexos e conteúdo confidencial.

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

Expor o localhost com uma URL HTTPS pública

Inicie o receptor e confira a porta local. Em outro terminal, execute npx portpreview 3000 e acrescente a rota ao endereço gerado, como https://YOUR-TUNNEL.portpreview.dev/webhooks/postmark. Mantenha o túnel ativo durante o teste. Ele oferece acesso público e HTTPS confiável, mas não autentica a Postmark; autenticação e validação do payload continuam obrigatórias. guia de segurança de túneis localhost

Configurar o webhook correto da Postmark

Para eventos de saída, escolha o Server e o Message Stream corretos, cadastre a URL em Webhooks e ative apenas os triggers implementados. O Inbound Message Stream tem sua própria inbound URL. A Webhooks API permite HttpAuth, HttpHeaders opcionais e triggers. O X-Postmark-Server-Token serve para administrar a API e não é enviado como credencial ao receptor. API de Webhooks

Autenticação da Postmark não é assinatura criptográfica

A Postmark não oferece verificação de assinatura HMAC para webhooks. Não existe signing secret para recalcular o digest do raw body nem header X-Postmark-Signature. Basic Authentication, IP allowlist e header secreto comprovam posse de uma credencial compartilhada, mas não a vinculam criptograficamente ao body. Use HTTPS, valide o payload e consulte faixas de IP atuais. Prefira HttpAuth; se usar https://username:[email protected]/path, crie credenciais exclusivas e fortes, codifique caracteres reservados e evite expor a URL. Nunca reutilize o Server API token.

Tratar eventos de entrega e bounce por tipo

Tire a lógica de negócio da requisição HTTP e deixe um worker processar os eventos persistidos de forma idempotente. Não determine uma supressão permanente só pelo nome de um campo: siga a classificação atual de Bounce e sua política de envio. Spam Complaint e Subscription Change são tipos próprios; Open e Click podem ocorrer várias vezes.

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

Projetar para novas tentativas e entregas duplicadas

A Postmark tenta novamente quando não recebe HTTP 200. Bounce e Inbound têm uma sequência mais longa do que Click, Open, Delivery e Subscription Change; 403 interrompe as tentativas. Um timeout depois do commit pode gerar uma duplicata legítima. Use uma restrição única sobre MessageID e, em endpoint misto, acrescente RecordType e um identificador como ID. Responda 200 depois de uma entrega durável mínima e processe APIs externas de forma assíncrona. novas tentativas e idempotência de webhooks

Testar eventos reais com segurança

Primeiro envie um POST sintético com curl. Depois gere eventos reais: Delivery para um endereço sob seu controle e Bounce pelos recursos de teste documentados pela Postmark, inclusive o black-hole test domain quando aplicável. Guarde o MessageID, envie duas vezes uma fixture sem dados sensíveis e confirme que o efeito ocorre uma única vez.

Resolver falhas comuns de webhook da Postmark

A requisição não chega ao endpoint

Confira o processo do túnel, a rota completa e a porta. Em caso de 401, compare as credenciais, reinicie o aplicativo e verifique se o proxy remove Authorization, sem imprimir o valor. Se houver novas tentativas depois do sucesso, observe status e latência no endpoint público: a Postmark exige 200. Se o payload mudar, confira RecordType, trigger e Inbound/Outbound; rejeite campos obrigatórios ausentes e aceite adições opcionais documentadas.

Todas as requisições retornam HTTP 401

guia de erros 401/403 em webhooks

Há novas tentativas após o processamento

Confira o processo do túnel, a rota completa e a porta. Em caso de 401, compare as credenciais, reinicie o aplicativo e verifique se o proxy remove Authorization, sem imprimir o valor. Se houver novas tentativas depois do sucesso, observe status e latência no endpoint público: a Postmark exige 200. Se o payload mudar, confira RecordType, trigger e Inbound/Outbound; rejeite campos obrigatórios ausentes e aceite adições opcionais documentadas.

O payload não corresponde ao exemplo

Confira o processo do túnel, a rota completa e a porta. Em caso de 401, compare as credenciais, reinicie o aplicativo e verifique se o proxy remove Authorization, sem imprimir o valor. Se houver novas tentativas depois do sucesso, observe status e latência no endpoint público: a Postmark exige 200. Se o payload mudar, confira RecordType, trigger e Inbound/Outbound; rejeite campos obrigatórios ausentes e aceite adições opcionais documentadas.

Checklist de segurança para produção

Use HTTPS e credenciais exclusivas e fortes de Basic Auth ou header secreto; separe API tokens e segredos; faça a rotação depois dos testes; remova URLs antigas; valide content type, tamanho, tipo, identificadores e campos; oculte dados de e-mail e credenciais nos logs; aplique privilégio mínimo e monitore falhas, atraso, duplicatas e dead letters.

guia para depurar webhooks localmente

Perguntas frequentes

Como testar um webhook da Postmark no localhost?
Execute o handler local, exponha a porta com <code>npx portpreview PORT</code> e cadastre a rota HTTPS no Message Stream correto. Proteja com Basic Authentication ou header secreto, valide o JSON, salve de forma idempotente e responda HTTP 200 rapidamente.
A Postmark assina webhooks com HMAC?
Não. Use HTTPS, Basic Authentication e valide cada payload.
Por que o mesmo webhook é reenviado?
Sem HTTP 200 há nova tentativa; grave uma chave única.
Delivery significa que o e-mail foi lido?
Não. Significa apenas que o servidor de destino aceitou o e-mail.