Todos los artículos
Probar webhooks de Postmark en local con HTTPS
Postmarkemail webhookslocalhostwebhook security

Probar webhooks de Postmark en local con HTTPS

Para probar un webhook de Postmark en localhost, ejecuta el handler local, expón su puerto con npx portpreview PORT y registra la ruta HTTPS resultante en el Message Stream adecuado. Protege el endpoint con Basic Authentication o un header secreto, valida el JSON, persístelo de forma idempotente y responde HTTP 200 cuanto antes.

Qué envía Postmark a un webhook

Postmark hace un HTTP POST cuando ocurre un evento. Los streams salientes notifican delivery, bounce, open, click, spam complaint y cambios de suscripción; los entrantes envían el correo ya analizado. Enruta por RecordType y valida el schema de cada tipo. Delivery solo significa que el servidor de destino aceptó el mensaje, no que llegó al inbox. Bounce incluye Type, TypeCode, Inactive y CanActivate. documentación general de webhooks referencia del webhook de rebotes

Crear un receptor Express local y pequeño

El ejemplo usa Express en el puerto 3000, comprueba Basic Auth antes del JSON, valida el sobre mínimo y guarda una clave de deduplicación antes de confirmar. Sustituye los helpers por una transacción o cola duradera. Limita el tamaño y no registres correos completos: pueden contener datos personales, enlaces de acceso, adjuntos o información 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);

Exponer localhost con una URL HTTPS pública

Arranca el receptor, comprueba el puerto y ejecuta npx portpreview 3000 en otra terminal. Añade la ruta, por ejemplo https://YOUR-TUNNEL.portpreview.dev/webhooks/postmark, y mantén el túnel activo. El túnel aporta acceso público y HTTPS, pero no autentica a Postmark; siguen siendo necesarias la autenticación y la validación. guía de seguridad de túneles localhost

Configurar el webhook correcto de Postmark

En Postmark selecciona el Server y Message Stream saliente correctos, agrega la URL en Webhooks y habilita solo los triggers implementados. El correo entrante usa la URL propia de Inbound Message Stream. Webhooks API admite HttpAuth, HttpHeaders y triggers. X-Postmark-Server-Token administra la API; no es una credencial enviada al receptor. API de Webhooks

La autenticación de Postmark no es una firma criptográfica

Postmark no admite actualmente firmas HMAC de webhooks: no hay signing secret para recalcular el raw body ni header X-Postmark-Signature. Basic Authentication, una allowlist IP o un header secreto prueban que se posee una credencial compartida, pero no la vinculan criptográficamente al body. Usa HTTPS, valida el payload y consulta rangos IP vigentes. Prefiere HttpAuth; si usas https://username:[email protected]/path, crea credenciales fuertes, codifica caracteres y evita filtrar la URL. Nunca reutilices el Server API token.

Procesar entregas y rebotes según su tipo

Saca la lógica de negocio de la petición HTTP y deja que un worker procese los eventos persistidos de forma idempotente. No deduzcas una supresión permanente solo por un campo: aplica la clasificación actual de bounce y tu política. Spam complaint y subscription change tienen tipos propios; open y click pueden repetirse.

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

Diseñar para reintentos y entregas duplicadas

Postmark reintenta si no recibe HTTP 200. Bounce e inbound siguen una secuencia más larga que click, open, delivery y subscription change; 403 detiene los reintentos. Un timeout después del commit produce un duplicado legítimo. Usa una restricción única sobre MessageID y, en endpoints mixtos, añade RecordType y un identificador como ID. Responde 200 tras una entrega duradera mínima y procesa APIs externas de forma asíncrona. reintentos e idempotencia de webhooks

Probar eventos reales de forma segura

Primero manda un POST sintético con curl. Después genera eventos reales: delivery hacia una dirección bajo tu control y bounce mediante las herramientas documentadas por Postmark, incluido el black-hole test domain cuando corresponda. Guarda MessageID, envía dos veces un fixture saneado y verifica un único efecto.

Resolver fallos habituales de webhooks de Postmark

El endpoint no recibe solicitudes

Revisa el proceso del túnel, la ruta completa y el puerto. Ante 401 compara las credenciales, reinicia la app y comprueba que el proxy no elimine Authorization, sin imprimirlo. Si hay reintentos tras procesar, observa status y latency públicos: Postmark exige 200. Si cambia el payload, revisa RecordType, trigger e inbound/outbound; rechaza campos obligatorios ausentes y tolera adiciones documentadas.

Todas las solicitudes devuelven HTTP 401

guía de errores 401/403

Hay reintentos después de procesar

Revisa el proceso del túnel, la ruta completa y el puerto. Ante 401 compara las credenciales, reinicia la app y comprueba que el proxy no elimine Authorization, sin imprimirlo. Si hay reintentos tras procesar, observa status y latency públicos: Postmark exige 200. Si cambia el payload, revisa RecordType, trigger e inbound/outbound; rechaza campos obligatorios ausentes y tolera adiciones documentadas.

El payload no coincide

Revisa el proceso del túnel, la ruta completa y el puerto. Ante 401 compara las credenciales, reinicia la app y comprueba que el proxy no elimine Authorization, sin imprimirlo. Si hay reintentos tras procesar, observa status y latency públicos: Postmark exige 200. Si cambia el payload, revisa RecordType, trigger e inbound/outbound; rechaza campos obligatorios ausentes y tolera adiciones documentadas.

Lista de seguridad para producción

Usa HTTPS y credenciales Basic Auth o header dedicado de alta entropía; separa tokens API y secretos; rota tras las pruebas; elimina URLs antiguas; valida content type, tamaño, tipo e identificadores; redacta datos de correo y secretos en logs; aplica privilegio mínimo y monitoriza fallos, lag, duplicados y dead letters.

guía para depurar webhooks localmente

Preguntas frecuentes

¿Cómo pruebo un webhook de Postmark en localhost?
Ejecuta el handler local, expón el puerto con npx portpreview PORT, añade la ruta a la URL HTTPS generada y configúrala en el Message Stream correcto.
¿Postmark firma los webhooks con HMAC?
No. La documentación actual no admite firmas HMAC. Usa HTTPS con Basic Authentication, opcionalmente la allowlist IP vigente, y valida cada payload.
¿Por qué Postmark vuelve a enviar el mismo webhook?
Reintenta cuando no recibe HTTP 200. Guarda una clave estable con una restricción única para absorber duplicados.
¿Delivery significa que el destinatario leyó el correo?
No. Solo indica que el servidor de destino lo aceptó; no demuestra inbox, apertura ni lectura.