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.
POST api.postmarkapp.com/emailX-Postmark-Server-TokenPOST api.postal.so/emailsAuthorization: BearerCambia 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.
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.
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 "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"}]+ }'- 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.
| Postmark | En Postal | Qué cambia |
|---|---|---|
X-Postmark-Server-Token | Authorization: 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. |
From | from | Postmark 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, Bcc | cc, bcc | Texto o lista. |
Subject | subject | En Postmark el asunto es opcional. En Postal es obligatorio cuando no hay plantilla. |
HtmlBody, TextBody | html, text | Como en Postmark, uno de los dos es obligatorio. |
ReplyTo | reply_to | Texto 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. |
Metadata | tags | Para 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, TrackLinks | open_tracking y click_tracking del dominio | En 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, ContentID | attachments: filename, content, content_type, content_id | Tambié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. |
MessageStream | No existe | Consulta el paso sobre Message Streams. |
POST /email/batch: hasta 500 mensajes | POST /emails/batch: hasta 100 correos electrónicos | Sin 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/withTemplate | POST /emails con template | template.id acepta el ID o el alias. Consulta el paso de plantillas. |
POST /email/batchWithTemplates | POST /emails/batch con template en cada elemento | Cada elemento lleva su propio template.variables. |
InlineCss | Sin campo equivalente | La documentación de Postal no describe esta opción. |
Respuesta: MessageID, ErrorCode, Message | Respuesta: {"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ío | Postal quita las direcciones suprimidas del envío y genera email.suppressed, sin error. |
POSTMARK_API_TEST en lugar del token | Direcciones de prueba | Envías a [email protected] y a las otras direcciones de prueba. No consume cuota y genera webhooks reales. Depende del plan. |
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.
- <p>¡Hola, {{name}}!</p>- <p>Tu pedido: {{order.id}}</p>+ <p>¡Hola, {{{NAME}}}!</p>+ <p>Tu pedido: {{{ORDER}}}</p>- "TemplateAlias": "pedido-enviado",- "TemplateModel": { "name": "Ana", "order": { "id": 10482 } }+ "template": {+ "id": "pedido-enviado",+ "variables": { "NAME": "Ana", "ORDER": "10482" }+ }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.
outboundPOST /emails, por la API o por SMTP.email.received.stream: transactional, y filtra por la etiqueta en tu código.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.
- 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 },| Postmark | Postal | |
|---|---|---|
| Servidor | smtp.postmarkapp.com (broadcasts: smtp-broadcasts.postmarkapp.com) | smtp.postal.so |
| Puertos | 25, 2525 o 587, con STARTTLS | 465 con TLS (recomendado) o 587 con STARTTLS. Si la red lo bloquea, 2465 (TLS) o 2587 (STARTTLS). |
| Usuario y contraseña | El token del servidor en ambos, o clave de acceso y clave secreta de un token SMTP | Usuario postal y una clave de API (ps_…) como contraseña. Basta una clave solo de envío. |
| Opciones por encabezado | X-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. |
| Habilitar | En la configuración del servidor | El SMTP forma parte del plan. Sin él, el mensaje se rechaza con 550 5.7.1. |
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-ides 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.
| Webhook de Postmark | Evento en Postal | Observación |
|---|---|---|
| Delivery | email.delivered | El servidor del destinatario aceptó el mensaje. |
| Rebote | email.bounced | En 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 complaint | email.complained | La dirección entra en la lista de supresión. |
| Open | email.opened | Requiere el seguimiento de aperturas activado en el dominio. |
| Click | email.clicked | Trae el enlace en data.click.link. |
| Subscription change | suppression.added y suppression.removed | Lo más cercano: avisan cuando una dirección entra o sale de la lista de supresión. Comprueba el contenido antes de cambiar. |
| Inbound | email.received | Llega cuando un buzón de entrada recibe un mensaje. Recibir en tu dominio es de los planes de pago. |
| Postmark | Postal | |
|---|---|---|
| Seguridad | Sin 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. |
| Formato | RecordType y los campos del nivel superior | type, created_at y data |
| Cuál es el correo | MessageID | data.email_id, el mismo id devuelto en el envío |
| Destinatario | Recipient o Email. Un mensaje para varias personas genera varios eventos. | data.to, una lista. Trátalo como lista. |
| Etiqueta y metadatos | Tag y Metadata | data.tags, un objeto nombre → valor |
| Motivo del rebote | Details, Type y TypeCode | data.bounce (type, subType, message, diagnosticCode) |
| Respuesta esperada | 200 | Cualquier 2xx en un máximo de 15 segundos |
| Nuevos intentos | Repite 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. |
- 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+ }{
"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"
}
}
}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.
- 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 Postmark | En Postal | Cómo llevarlo |
|---|---|---|
Supresiones por stream (/message-streams/{stream_id}/suppressions) | Una lista por equipo | Junta las listas de todos los streams y envíalas a Postal. Una dirección suprimida queda bloqueada para todos los envíos del equipo. |
HardBounce | Origen bounce | Lo que añades por la API entra como manual; los orígenes bounce y complaint los registra Postal en sus propios envíos. |
SpamComplaint | Origen complaint | Impórtalas a la lista de supresión y mantenlas allí. |
ManualSuppression | Origen manual | Entra por el mismo endpoint. |
| Hasta 50 direcciones por llamada | Hasta 100 direcciones por llamada | POST /suppressions/batch/add, con clave de acceso total. |
Origen Recipient (la propia persona) | Contacto con unsubscribed: true | Quien 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. |
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.
Migrar a Postal
Otras guías
Cada plataforma tiene sus diferencias. Elige la guía de la que usas hoy.
¿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.