Novedad: campañas, automatizaciones y correos del producto en la misma cuenta. Conoce Postal
Iniciar sesión Crear cuenta

De Postmark a Postal, y lo que no tiene equivalente

El envío por JSON es parecido, pero los nombres de los campos, el encabezado de la clave, los webhooks y la idea de Message Streams son diferentes. Esta guía muestra qué tiene correspondencia y qué debes organizar de otra manera.

Message Streams
Sin streams
Autenticación
Token para Bearer
Seguridad
Webhooks firmados

Visión general

En Postmark envías con POST /email, con el token del servidor en X-Postmark-Server-Token y campos en PascalCase. En Postal el envío es POST /emails, con Authorization: Bearer ps_… y campos en snake_case. El diseño es parecido, pero hay una diferencia de modelo: Postal no tiene Message Streams. Quien separa lo transaccional del marketing mediante streams debe organizarlo de otra manera, y el paso 4 muestra cómo.

PostmarkPOST api.postmarkapp.com/emailX-Postmark-Server-Token
PostalPOST api.postal.so/emailsAuthorization: Bearer

Cambia solo el nombre

Los campos del envío, los adjuntos, los encabezados y el SMTP tienen correspondencia. Los nombres pasan de PascalCase a snake_case, y el token va en Authorization: Bearer.

Cambia de lugar

El seguimiento pasa a ser del dominio. Los metadatos se convierten en etiquetas con caracteres restringidos. La seguridad de los webhooks pasa a ser la firma de cada entrega.

No tiene equivalente

Message Streams, supresiones separadas por stream, plantillas con bloques y datos anidados, el token de prueba y el lote de 500 mensajes.

Postal no imita la API de Postmark. La única API con modo de compatibilidad es la de Resend: si ya usas su SDK, consulta la guía de Resend. Para Postmark, el cambio se hace en el código, con fetch o con los SDK oficiales de Postal para Node.js y PHP.

1

Verifica el dominio y crea una clave

Añade el dominio en el panel (o con POST /domains), crea en tu DNS los registros que muestra Postal y solicita la verificación. Postal firma con DKIM en el selector postal y usa un subdominio postal para el Return-Path, así que comprueba que ninguno de tus registros use ya esos nombres y conserva los de Postmark hasta que termine el cambio.

  • El token del servidor, que solo envía, corresponde a una clave solo de envío. Quien además gestiona plantillas, supresiones y webhooks por API necesita una clave de acceso total.
  • La clave puede restringirse a un dominio, lo que ayuda a separar sistemas. El token completo aparece una única vez, al crearla.
  • Región: Postal envía desde São Paulo de forma predeterminada y permite elegir us-east-1, us-east-2, eu-west-1 o ap-northeast-1 en cada dominio.
2

Cambia la llamada de envío

Los campos cambian de nombre y la clave cambia de encabezado. También cambia la forma de saber si salió bien: Postmark responde con ErrorCode en el cuerpo, y Postal usa el estado HTTP y un error con statusCode, name y message. La respuesta de éxito es solo {"id": "…"}. Con el encabezado Idempotency-Key, repetir una solicitud no duplica el envío.

curl · antes y después
- curl "https://api.postmarkapp.com/email" \-   -X POST \-   -H "Accept: application/json" \-   -H "Content-Type: application/json" \-   -H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \-   -d '{-     "From": "[email protected]",-     "To": "[email protected]",-     "Subject": "Tu pedido #10482 salió para la entrega",-     "HtmlBody": "<p>El pedido <b>#10482</b> está en camino.</p>",-     "Tag": "envío",-     "MessageStream": "outbound"-   }'+ curl -X POST https://api.postal.so/emails \+   -H "Authorization: Bearer $POSTAL_API_KEY" \+   -H "Content-Type: application/json" \+   -d '{+     "from": "Aurora Cosméticos <[email protected]>",+     "to": ["[email protected]"],+     "subject": "Tu pedido #10482 salió para la entrega",+     "html": "<p>El pedido <b>#10482</b> está en camino.</p>",+     "tags": [{"name": "stream", "value": "transactional"}]+   }'
Node.js · antes y después
- const response = await fetch('https://api.postmarkapp.com/email', {-   method: 'POST',-   headers: {-     Accept: 'application/json',-     'Content-Type': 'application/json',-     'X-Postmark-Server-Token': process.env.POSTMARK_SERVER_TOKEN,-   },-   body: JSON.stringify({-     From: '[email protected]',-     To: '[email protected]',-     Subject: 'Tu pedido #10482 salió para la entrega',-     HtmlBody: '<p>El pedido <b>#10482</b> está en camino.</p>',-     MessageStream: 'outbound',-   }),- });- const { MessageID, ErrorCode } = await response.json();+ const response = await fetch('https://api.postal.so/emails', {+   method: 'POST',+   headers: {+     Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,+     'Content-Type': 'application/json',+     'Idempotency-Key': 'order-10482-shipped',+   },+   body: JSON.stringify({+     from: 'Aurora Cosméticos <[email protected]>',+     to: ['[email protected]'],+     subject: 'Tu pedido #10482 salió para la entrega',+     html: '<p>El pedido <b>#10482</b> está en camino.</p>',+     tags: [{ name: 'stream', value: 'transactional' }],+   }),+ });+ if (!response.ok) { /* estado HTTP y cuerpo { statusCode, name, message } */ }+ const { id } = await response.json();

Campo a campo

Todo lo que acepta el envío de Postmark y qué ocurre con cada parte en Postal.

Campos del envío: en qué se convierte cada uno.
PostmarkEn PostalQué cambia
X-Postmark-Server-TokenAuthorization: Bearer ps_…El Accept: application/json que exige Postmark no es necesario. La clave puede ser solo de envío o de acceso total, y puede restringirse a un dominio.
FromfromPostmark pide una Sender Signature confirmada. Postal solo acepta remitentes de un dominio verificado y con envío activo.
To (texto separado por comas)to (texto o lista)Hasta 50 direcciones. En Postmark el límite de 50 suma To, Cc y Bcc.
Cc, Bcccc, bccTexto o lista.
SubjectsubjectEn Postmark el asunto es opcional. En Postal es obligatorio cuando no hay plantilla.
HtmlBody, TextBodyhtml, textComo en Postmark, uno de los dos es obligatorio.
ReplyToreply_toTexto o lista.
Headers (lista de {Name, Value})headers (objeto){"X-Pedido": "10482"}. No se pueden sobrescribir From, To, Subject, Message-ID ni otros encabezados del sistema.
Tag (un texto, hasta 1000 caracteres)tags: lista de {name, value}Hasta 50 por correo electrónico. El nombre y el valor solo aceptan letras ASCII, números, _ y -, hasta 256 caracteres.
MetadatatagsPara valores cortos y simples, convierte cada par en una etiqueta; vuelve en data.tags en los webhooks. Los datos con espacios, acentos o más largos se quedan en tu base de datos, vinculados al id del correo electrónico.
TrackOpens, TrackLinksopen_tracking y click_tracking del dominioEn Postmark la elección es de cada mensaje, incluso HTML o texto. En Postal se activa o desactiva en el dominio y no existe un campo en el envío. Depende del plan.
Attachments: Name, Content, ContentType, ContentIDattachments: filename, content, content_type, content_idTambién acepta path, una URL pública. Hasta 40 MB en total (en Postmark, 10 MB por correo, adjuntos incluidos). Los adjuntos no valen en el lote.
MessageStreamNo existeConsulta el paso sobre Message Streams.
POST /email/batch: hasta 500 mensajesPOST /emails/batch: hasta 100 correos electrónicosSin adjuntos y sin programación. Todo se valida antes: entran todos o ninguno. En Postmark el lote responde 200 aunque haya fallos individuales.
POST /email/withTemplatePOST /emails con templatetemplate.id acepta el ID o el alias. Consulta el paso de plantillas.
POST /email/batchWithTemplatesPOST /emails/batch con template en cada elementoCada elemento lleva su propio template.variables.
InlineCssSin campo equivalenteLa documentación de Postal no describe esta opción.
Respuesta: MessageID, ErrorCode, MessageRespuesta: {"id": "…"}El éxito y el error llegan por el estado HTTP. El error tiene statusCode, name y message: comprueba el name, no el texto.
Destinatario inactivo (error 406)Quitado del envíoPostal quita las direcciones suprimidas del envío y genera email.suppressed, sin error.
POSTMARK_API_TEST en lugar del tokenDirecciones de pruebaEnvías a [email protected] y a las otras direcciones de prueba. No consume cuota y genera webhooks reales. Depende del plan.
3

Recrea las plantillas

Las plantillas de Postmark aceptan bloques y repeticiones, como {{#each person}}, y el modelo (TemplateModel) es un objeto que puede contener otros objetos. Las de Postal solo sustituyen variables, con el formato {{{NOME}}}, con tres llaves, y cada valor debe ser texto o número. Cuando tu plantilla depende de bloques, pasa la lógica al código: elige entre plantillas o arma tú mismo el HTML y envíalo en html.

  • En Postal editas un borrador y lo publicas; los envíos usan siempre la última versión publicada, y las versiones antiguas se pueden restaurar.
  • Postal también tiene layouts, para reutilizar diseños de correo electrónico. No los trates como idénticos a los layouts de Postmark: recréalos y pruébalos.
  • En el HTML los valores se escapan. Una variable sin valor y sin valor predeterminado (fallback_value) devuelve un error 422.
Variables de la plantilla
- <p>¡Hola, {{name}}!</p>- <p>Tu pedido: {{order.id}}</p>+ <p>¡Hola, {{{NAME}}}!</p>+ <p>Tu pedido: {{{ORDER}}}</p>
Envío con plantilla
- "TemplateAlias": "pedido-enviado",- "TemplateModel": { "name": "Ana", "order": { "id": 10482 } }+ "template": {+   "id": "pedido-enviado",+   "variables": { "NAME": "Ana", "ORDER": "10482" }+ }
4

Decide qué hacer con los Message Streams

En Postmark, un servidor tiene streams: transaccional (de forma predeterminada, outbound), de broadcasts y de entrada, hasta 10 en total, y las supresiones se guardan por stream. Postal no tiene ese objeto. Los tres usos tienen un camino, pero ninguno es un campo del envío, y la separación de supresiones no existe: la lista es única para el equipo.

  • Para aislar la reputación, usa dominios o subdominios distintos (uno para lo transaccional, otro para las campañas) y una clave restringida a cada dominio.
  • La pausa por una tasa alta de rebotes o quejas vale para todo el equipo, no para un dominio ni una etiqueta. Consulta los límites de reputación en Límites.
  • Si tu uso depende de streams separados con supresiones independientes, habla con nosotros antes de migrar.
Stream transaccionalEl predeterminado, outbound
Envío normalPOST /emails, por la API o por SMTP.
Stream de broadcastsBoletines y promociones
CampañasContactos, segmentos, temas y baja. El envío es una campaña, no un campo del correo electrónico.
Stream de entradaCorreos recibidos
Buzones de entradaConversaciones en la plataforma y el webhook email.received.
Supresiones por streamUna lista para cada stream
Una lista por equipoUn rebote en una campaña también bloquea la misma dirección en los correos transaccionales.
Webhooks y estadísticas por streamSeparados por stream
Webhooks del equipoMarca los envíos con una etiqueta, como stream: transactional, y filtra por la etiqueta en tu código.
5

Apunta el SMTP a Postal

Si algún sistema envía por SMTP, cambia servidor, usuario y contraseña y quita los encabezados X-PM-*. Los mensajes pasan por las mismas reglas de la API (dominio verificado, cuotas, lista de supresión) y aparecen en Correos y Registros.

Nodemailer · antes y después
- host: 'smtp.postmarkapp.com',- port: 587,- secure: false,- auth: { user: process.env.POSTMARK_SERVER_TOKEN, pass: process.env.POSTMARK_SERVER_TOKEN },+ host: 'smtp.postal.so',+ port: 465,+ secure: true,+ auth: { user: 'postal', pass: process.env.POSTAL_API_KEY },
PostmarkPostal
Servidorsmtp.postmarkapp.com (broadcasts: smtp-broadcasts.postmarkapp.com)smtp.postal.so
Puertos25, 2525 o 587, con STARTTLS465 con TLS (recomendado) o 587 con STARTTLS. Si la red lo bloquea, 2465 (TLS) o 2587 (STARTTLS).
Usuario y contraseñaEl token del servidor en ambos, o clave de acceso y clave secreta de un token SMTPUsuario postal y una clave de API (ps_…) como contraseña. Basta una clave solo de envío.
Opciones por encabezadoX-PM-Message-Stream, X-PM-Tag y X-PM-Metadata-*Por SMTP no hay etiquetas, programación ni plantillas. Esos encabezados viajarían en el mensaje como encabezados comunes: quítalos.
HabilitarEn la configuración del servidorEl SMTP forma parte del plan. Sin él, el mensaje se rechaza con 550 5.7.1.
6

Registra el webhook y comprueba la firma

Crea el endpoint en Postal (POST /webhooks, con URL HTTPS y la lista de eventos) y guarda el signing_secret. El cambio de fondo es la seguridad: Postmark no firma las entregas, y Postal firma todas. Calcula la firma sobre el cuerpo exactamente como llegó, sin leer y reescribir el JSON.

  • El webhook-id es el mismo en todos los intentos: guarda los ya procesados para descartar duplicados.
  • Responde 2xx enseguida y procesa después, en una cola.
  • Para probar sin enviar de verdad, envía correos electrónicos a las direcciones de prueba o usa "Enviar evento de prueba" en el panel.
Postal también avisa de email.sent, email.delivery_delayed, email.failed, email.suppressed, email.replied y otros.
Webhook de PostmarkEvento en PostalObservación
Deliveryemail.deliveredEl servidor del destinatario aceptó el mensaje.
Reboteemail.bouncedEn data.bounce.type: Permanent, Transient o Undetermined. Postmark usa tipos como HardBounce y Transient. Un rebote permanente pone la dirección en la lista de supresión.
Spam complaintemail.complainedLa dirección entra en la lista de supresión.
Openemail.openedRequiere el seguimiento de aperturas activado en el dominio.
Clickemail.clickedTrae el enlace en data.click.link.
Subscription changesuppression.added y suppression.removedLo más cercano: avisan cuando una dirección entra o sale de la lista de supresión. Comprueba el contenido antes de cambiar.
Inboundemail.receivedLlega cuando un buzón de entrada recibe un mensaje. Recibir en tu dominio es de los planes de pago.
PostmarkPostal
SeguridadSin firma: Postmark no ofrece verificación HMAC. La protección es la autenticación básica en la URL y la lista de IP de Postmark.Cada entrega va firmada (Standard Webhooks): encabezados webhook-id, webhook-timestamp y webhook-signature, con el whsec_… del endpoint. Quien confiaba en la URL pasa a comprobar la firma.
FormatoRecordType y los campos del nivel superiortype, created_at y data
Cuál es el correoMessageIDdata.email_id, el mismo id devuelto en el envío
DestinatarioRecipient o Email. Un mensaje para varias personas genera varios eventos.data.to, una lista. Trátalo como lista.
Etiqueta y metadatosTag y Metadatadata.tags, un objeto nombre → valor
Motivo del reboteDetails, Type y TypeCodedata.bounce (type, subType, message, diagnosticCode)
Respuesta esperada200Cualquier 2xx en un máximo de 15 segundos
Nuevos intentosRepite 5xx, 408, 429 y fallos de red, a 1, 5, 10, 10, 10 y 15 minutos. Los demás 4xx descartan el evento.Toda respuesta que no sea 2xx cuenta como fallo. 8 intentos en unas 27 horas, con reenvío desde el panel o con POST /webhooks/{id}/events/{event_id}/replay.
Tratamiento · antes y después
- if (req.body.RecordType === 'Bounce') {-   // req.body.Email, req.body.MessageID- }+ const event = verifyWebhook(req.body, req.headers, process.env.POSTAL_WEBHOOK_SECRET);+ if (event.type === 'email.bounced') {+   // event.data.to, event.data.email_id+ }
email.bounced · lo que llega a tu servidor
{
  "type": "email.bounced",
  "created_at": "2026-10-24T13:02:15.004Z",
  "data": {
    "email_id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
    "to": ["[email protected]"],
    "subject": "Tu pedido #10482 salió para la entrega",
    "tags": { "stream": "transactional" },
    "bounce": {
      "type": "Permanent",
      "subType": "General",
      "message": "smtp; 550 5.1.1 user unknown"
    }
  }
}
200Responde 2xx en un máximo de 15 s
7

Lleva las supresiones

Hazlo antes del primer envío real, para no escribir a quien ya pidió la baja ni repetir rebotes. Postal pausa los envíos del equipo si los rebotes permanentes superan el límite del nivel de la reputación de la cuenta (3% en una cuenta nueva) o las quejas superan el 0,3%; por eso importa una lista limpia. El ejemplo lee la lista de un stream de Postmark y la envía a Postal en bloques de 100. Repite para cada stream y usa una clave de acceso total.

Node.js · leer en Postmark y enviar a Postal
- const base = 'https://api.postmarkapp.com/message-streams';- const dump = await fetch(`${base}/${stream}/suppressions/dump`, {-   headers: {-     Accept: 'application/json',-     'X-Postmark-Server-Token': process.env.POSTMARK_SERVER_TOKEN,-   },- }).then((r) => r.json());- const emails = dump.Suppressions.map((s) => s.EmailAddress);+ for (let i = 0; i < emails.length; i += 100) {+   await fetch('https://api.postal.so/suppressions/batch/add', {+     method: 'POST',+     headers: {+       Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,+       'Content-Type': 'application/json',+     },+     body: JSON.stringify({ emails: emails.slice(i, i + 100) }),+   });+ }
En PostmarkEn PostalCómo llevarlo
Supresiones por stream (/message-streams/{stream_id}/suppressions)Una lista por equipoJunta las listas de todos los streams y envíalas a Postal. Una dirección suprimida queda bloqueada para todos los envíos del equipo.
HardBounceOrigen bounceLo que añades por la API entra como manual; los orígenes bounce y complaint los registra Postal en sus propios envíos.
SpamComplaintOrigen complaintImpórtalas a la lista de supresión y mantenlas allí.
ManualSuppressionOrigen manualEntra por el mismo endpoint.
Hasta 50 direcciones por llamadaHasta 100 direcciones por llamadaPOST /suppressions/batch/add, con clave de acceso total.
Origen Recipient (la propia persona)Contacto con unsubscribed: trueQuien pidió salir de los correos de marketing no debe entrar en la lista de supresión, porque bloquea también los transaccionales. Usa POST /contacts/batch.
8

Cambia el tráfico poco a poco

Los ID son diferentes: el MessageID de un correo de Postmark no existe en Postal. Durante el cambio, guarda de qué proveedor salió cada correo y sigue recibiendo los webhooks de Postmark para lo que salió por allí. Envía cada flujo por un solo proveedor, para no duplicar ni pausar todo por culpa de correos equivocados.

Antes de cambiar la clave

  • Dominio verificado en Postal, con los registros antiguos todavía en su sitio.
  • Clave solo de envío, restringida al dominio, fuera del código y del repositorio.
  • Decisión tomada sobre cada Message Stream: envío normal, campaña o buzón de entrada.
  • Supresiones de todos los streams importadas, y bajas de marketing importadas como contactos dados de baja.
  • Plantillas recreadas con {{{VARIÁVEL}}}, publicadas, y la lógica de bloques movida al código.
  • Metadatos que van en etiquetas renombrados con letras, números, _ y -.
  • Webhook creado, firma comprobada con el cuerpo sin procesar y respuesta 2xx en un máximo de 15 segundos.
  • Pruebas hechas con [email protected] y [email protected].
  • Un flujo cada vez, vigilando los rebotes los primeros días.

Preguntas frecuentes

¿Postal tiene Message Streams?

No. En Postal, la separación entre correo transaccional y de marketing se hace con servicios diferentes (envío por API o SMTP por un lado, campañas y contactos por otro), con dominios separados y con etiquetas. La lista de supresión y los webhooks son de todo el equipo.

¿Puedo mantener Postmark mientras migro?

Sí, y es lo más seguro. Usa las dos cuentas en paralelo y cambia un flujo cada vez. Quita los registros de Postmark solo cuando ya no salga nada por allí.

¿Viene incluido el historial de eventos de Postmark?

No. Postal no importa correos ni eventos de otra plataforma, y guarda los tuyos durante 30 días. Exporta lo que necesites antes de dar de baja Postmark.

¿Vas a migrar desde Postmark?

Crea tu cuenta y empieza con el plan Gratis. Para planificar la cuota, el calentamiento y el orden de la migración, habla con nosotros por el formulario de contacto con el dominio y el volumen diario.