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.
