Do Postmark para a Postal, e o que não tem par
O envio por JSON é parecido, mas os nomes dos campos, o cabeçalho da chave, os webhooks e a ideia de Message Streams são diferentes. Este guia mostra o que tem correspondente e o que você precisa organizar de outro jeito.
- Message Streams
- Sem streams
- Autenticação
- Token para Bearer
- Segurança
- Webhooks assinados
Visão geral
No Postmark você envia com POST /email, com o token do servidor em X-Postmark-Server-Token e campos em PascalCase. Na Postal o envio é POST /emails, com Authorization: Bearer ps_… e campos em snake_case. O desenho é parecido, mas há uma diferença de modelo: a Postal não tem Message Streams. Quem separa transacional de marketing por streams precisa organizar isso de outro jeito, e o passo 4 mostra como.
POST api.postmarkapp.com/emailX-Postmark-Server-TokenPOST api.postal.so/emailsAuthorization: BearerMuda só o nome
Os campos do envio, os anexos, os cabeçalhos e o SMTP têm correspondente. Os nomes passam de PascalCase para snake_case, e o token vai em Authorization: Bearer.
Muda de lugar
Rastreamento passa a ser do domínio. Metadados viram tags com caracteres restritos. A segurança dos webhooks passa a ser a assinatura de cada entrega.
Não tem equivalente
Message Streams, supressões separadas por stream, templates com blocos e dados aninhados, o token de teste e o lote de 500 mensagens.
A Postal não imita a API do Postmark. A única API com modo de compatibilidade é a da Resend: se você já usa o SDK dela, veja o guia da Resend. Para o Postmark, a troca é no código, com fetch ou com os SDKs oficiais da Postal para Node.js e PHP.
Verifique o domínio e crie uma chave
Adicione o domínio no painel (ou com POST /domains), crie na sua DNS os registros que a Postal mostra e peça a verificação. A Postal assina com DKIM no seletor postal e usa um subdomínio postal para o Return-Path, então confira que nenhum registro seu já usa esses nomes e mantenha os do Postmark até a virada terminar.
- O token do servidor, que só envia, corresponde a uma chave só de envio. Quem também gerencia templates, supressões e webhooks por API precisa de uma chave de acesso total.
- A chave pode ser restrita a um domínio, o que ajuda a separar sistemas. O token completo aparece uma única vez, na criação.
- Região: a Postal envia de São Paulo por padrão e deixa escolher us-east-1, us-east-2, eu-west-1 ou ap-northeast-1 em cada domínio.
Troque a chamada de envio
Os campos mudam de nome e a chave muda de cabeçalho. Muda também o jeito de saber se deu certo: o Postmark responde com ErrorCode no corpo, e a Postal usa o status HTTP e um erro com statusCode, name e message. A resposta de sucesso é só {"id": "…"}. Com o cabeçalho Idempotency-Key, repetir uma requisição não duplica o envio.
- 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": "Seu pedido #10482 saiu para entrega",- "HtmlBody": "<p>O pedido <b>#10482</b> está a caminho.</p>",- "Tag": "envio",- "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": "Seu pedido #10482 saiu para entrega",+ "html": "<p>O pedido <b>#10482</b> está a caminho.</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: 'Seu pedido #10482 saiu para entrega',- HtmlBody: '<p>O pedido <b>#10482</b> está a caminho.</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: 'Seu pedido #10482 saiu para entrega',+ html: '<p>O pedido <b>#10482</b> está a caminho.</p>',+ tags: [{ name: 'stream', value: 'transactional' }],+ }),+ });+ if (!response.ok) { /* status HTTP e corpo { statusCode, name, message } */ }+ const { id } = await response.json();Campo a campo
Tudo o que o envio do Postmark aceita e o que acontece com cada parte na Postal.
| Postmark | Na Postal | O que muda |
|---|---|---|
X-Postmark-Server-Token | Authorization: Bearer ps_… | O Accept: application/json que o Postmark exige não é necessário. A chave pode ser só de envio ou de acesso total, e pode ser restrita a um domínio. |
From | from | O Postmark pede uma Sender Signature confirmada. A Postal só aceita remetentes de um domínio verificado e com envio ativo. |
To (texto separado por vírgula) | to (texto ou lista) | Até 50 endereços. No Postmark o limite de 50 soma To, Cc e Bcc. |
Cc, Bcc | cc, bcc | Texto ou lista. |
Subject | subject | No Postmark o assunto é opcional. Na Postal é obrigatório quando não há template. |
HtmlBody, TextBody | html, text | Como no Postmark, um dos dois é obrigatório. |
ReplyTo | reply_to | Texto ou lista. |
Headers (lista de {Name, Value}) | headers (objeto) | {"X-Pedido": "10482"}. Não dá para sobrescrever From, To, Subject, Message-ID e outros cabeçalhos de sistema. |
Tag (um texto, até 1000 caracteres) | tags: lista de {name, value} | Até 50 por e-mail. Nome e valor só aceitam letras ASCII, números, _ e -, até 256 caracteres. |
Metadata | tags | Para valores curtos e simples, vire cada par uma tag; ela volta em data.tags nos webhooks. Dados com espaços, acentos ou mais longos ficam no seu banco, ligados ao id do e-mail. |
TrackOpens, TrackLinks | open_tracking e click_tracking do domínio | No Postmark a escolha é de cada mensagem, inclusive HTML ou texto. Na Postal é ligado ou desligado no domínio e não existe campo no envio. Depende do plano. |
Attachments: Name, Content, ContentType, ContentID | attachments: filename, content, content_type, content_id | Também aceita path, uma URL pública. Até 40 MB no total (no Postmark, 10 MB por e-mail, anexos incluídos). Anexos não valem no lote. |
MessageStream | Não existe | Veja o passo sobre Message Streams. |
POST /email/batch: até 500 mensagens | POST /emails/batch: até 100 e-mails | Sem anexos e sem agendamento. Tudo é validado antes: entram todos ou nenhum. No Postmark o lote responde 200 mesmo com falhas individuais. |
POST /email/withTemplate | POST /emails com template | template.id aceita o ID ou o alias. Veja o passo de templates. |
POST /email/batchWithTemplates | POST /emails/batch com template em cada item | Cada item leva o próprio template.variables. |
InlineCss | Sem campo igual | A documentação da Postal não descreve essa opção. |
Resposta: MessageID, ErrorCode, Message | Resposta: {"id": "…"} | O sucesso e o erro vêm pelo status HTTP. O erro tem statusCode, name e message: teste o name, não o texto. |
| Destinatário inativo (erro 406) | Removido do envio | A Postal tira os endereços suprimidos do envio e gera email.suppressed, sem erro. |
POSTMARK_API_TEST no lugar do token | Endereços de teste | Você manda para [email protected] e os outros endereços de teste. Não usa cota e gera webhooks reais. Depende do plano. |
Recrie os templates
Os templates do Postmark aceitam blocos e repetições, como {{#each person}}, e o modelo (TemplateModel) é um objeto que pode ter outros objetos dentro. Os da Postal só substituem variáveis, no formato {{{NOME}}}, com três chaves, e cada valor precisa ser texto ou número. Quando o seu template depende de blocos, mova a lógica para o código: escolha entre templates ou monte o HTML você mesmo e mande em html.
- Na Postal você edita um rascunho e publica; os envios usam sempre a última versão publicada, e as versões antigas podem ser restauradas.
- A Postal também tem layouts, para reaproveitar desenhos de e-mail. Não os trate como idênticos aos layouts do Postmark: recrie e teste.
- No HTML os valores são escapados. Uma variável sem valor e sem valor padrão (
fallback_value) devolve erro 422.
- <p>Olá, {{name}}!</p>- <p>Seu pedido: {{order.id}}</p>+ <p>Olá, {{{NAME}}}!</p>+ <p>Seu pedido: {{{ORDER}}}</p>- "TemplateAlias": "pedido-enviado",- "TemplateModel": { "name": "Ana", "order": { "id": 10482 } }+ "template": {+ "id": "pedido-enviado",+ "variables": { "NAME": "Ana", "ORDER": "10482" }+ }Decida o que fazer com os Message Streams
No Postmark, um servidor tem streams: transacional (por padrão, outbound), de broadcasts e de entrada, até 10 no total, e as supressões são guardadas por stream. A Postal não tem esse objeto. Os três usos têm um caminho, mas nenhum é um campo no envio, e a separação de supressões não existe: a lista é única para o time.
- Para isolar a reputação, use domínios ou subdomínios diferentes (um para o transacional, outro para as campanhas) e uma chave restrita a cada domínio.
- A pausa por taxa alta de bounces ou reclamações vale para o time inteiro, não para um domínio ou uma tag. Veja os limites de reputação em Limites.
- Se o seu uso depende de streams separados com supressões independentes, converse com a gente antes de migrar.
outboundPOST /emails, pela API ou pelo SMTP.email.received.stream: transactional, e filtre pela tag no seu código.Aponte o SMTP para a Postal
Se algum sistema envia por SMTP, troque servidor, usuário e senha e tire os cabeçalhos X-PM-*. As mensagens passam pelas mesmas regras da API (domínio verificado, cotas, lista de supressão) e aparecem em E-mails e Logs.
- 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 |
| Portas | 25, 2525 ou 587, com STARTTLS | 465 com TLS (recomendada) ou 587 com STARTTLS. Se a rede bloquear, 2465 (TLS) ou 2587 (STARTTLS). |
| Usuário e senha | O token do servidor nos dois, ou chave de acesso e chave secreta de um token SMTP | Usuário postal e uma chave de API (ps_…) como senha. Uma chave só de envio basta. |
| Opções por cabeçalho | X-PM-Message-Stream, X-PM-Tag e X-PM-Metadata-* | Pelo SMTP não há tags, agendamento nem templates. Esses cabeçalhos seguiriam na mensagem como cabeçalhos comuns: tire-os. |
| Habilitar | Nas configurações do servidor | O SMTP faz parte do plano. Sem ele a mensagem é recusada com 550 5.7.1. |
Cadastre o webhook e confira a assinatura
Crie o endpoint na Postal (POST /webhooks, com URL HTTPS e a lista de eventos) e guarde o signing_secret. A mudança de fundo é a segurança: o Postmark não assina as entregas, e a Postal assina todas. Calcule a assinatura sobre o corpo exatamente como chegou, sem ler e reescrever o JSON.
- O
webhook-idé o mesmo em todas as tentativas: guarde os já processados para descartar duplicatas. - Responda 2xx logo e processe depois, numa fila.
- Para testar sem enviar de verdade, mande e-mails para os endereços de teste ou use "Enviar evento de teste" no painel.
| Webhook do Postmark | Evento na Postal | Observação |
|---|---|---|
| Delivery | email.delivered | O servidor do destinatário aceitou a mensagem. |
| Bounce | email.bounced | Em data.bounce.type: Permanent, Transient ou Undetermined. O Postmark usa tipos como HardBounce e Transient. Bounce permanente coloca o endereço na lista de supressão. |
| Spam complaint | email.complained | O endereço entra na lista de supressão. |
| Open | email.opened | Precisa do rastreamento de aberturas ligado no domínio. |
| Click | email.clicked | Traz o link em data.click.link. |
| Subscription change | suppression.added e suppression.removed | O mais próximo: avisam quando um endereço entra ou sai da lista de supressão. Confira o conteúdo antes de trocar. |
| Inbound | email.received | Chega quando uma inbox recebe uma mensagem. Receber no seu domínio é dos planos pagos. |
| Postmark | Postal | |
|---|---|---|
| Segurança | Sem assinatura: o Postmark não oferece verificação HMAC. A proteção é a autenticação básica na URL e a lista de IPs do Postmark. | Cada entrega é assinada (Standard Webhooks): cabeçalhos webhook-id, webhook-timestamp e webhook-signature, com o whsec_… do endpoint. Quem confiava na URL passa a conferir a assinatura. |
| Formato | RecordType e os campos no nível de cima | type, created_at e data |
| Qual e-mail foi | MessageID | data.email_id, o mesmo id devolvido no envio |
| Destinatário | Recipient ou Email. Uma mensagem para várias pessoas gera vários eventos. | data.to, uma lista. Trate como lista. |
| Tag e metadados | Tag e Metadata | data.tags, um objeto nome → valor |
| Motivo do bounce | Details, Type e TypeCode | data.bounce (type, subType, message, diagnosticCode) |
| Resposta esperada | 200 | Qualquer 2xx em até 15 segundos |
| Novas tentativas | Repete 5xx, 408, 429 e falhas de rede, em 1, 5, 10, 10, 10 e 15 minutos. Os outros 4xx descartam o evento. | Toda resposta fora de 2xx conta como falha. 8 tentativas em cerca de 27 horas, com reenvio pelo painel ou por 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": "Seu pedido #10482 saiu para entrega",
"tags": { "stream": "transactional" },
"bounce": {
"type": "Permanent",
"subType": "General",
"message": "smtp; 550 5.1.1 user unknown"
}
}
}Leve as supressões
Faça isso antes do primeiro envio real, para não escrever a quem já pediu para sair nem repetir bounces. A Postal pausa os envios do time se os bounces permanentes passam do limite do nível da reputação da conta (3% numa conta nova) ou as reclamações passam de 0,3%; por isso a lista limpa importa. O exemplo lê a lista de um stream no Postmark e a envia à Postal em blocos de 100. Repita para cada stream e use uma chave de acesso 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) }),+ });+ }| No Postmark | Na Postal | Como levar |
|---|---|---|
Supressões por stream (/message-streams/{stream_id}/suppressions) | Uma lista por time | Junte as listas de todos os streams e envie para a Postal. Um endereço suprimido fica bloqueado para todos os envios do time. |
HardBounce | Origem bounce | O que você adiciona pela API entra como manual; as origens bounce e complaint são registradas pela Postal nos envios dela. |
SpamComplaint | Origem complaint | Importe para a lista de supressão e mantenha lá. |
ManualSuppression | Origem manual | Entra pelo mesmo endpoint. |
| Até 50 endereços por chamada | Até 100 endereços por chamada | POST /suppressions/batch/add, com chave de acesso total. |
Origem Recipient (a própria pessoa) | Contato com unsubscribed: true | Quem pediu para sair de e-mails de marketing não deve entrar na lista de supressão, porque ela bloqueia também os transacionais. Use POST /contacts/batch. |
Vire o tráfego aos poucos
Os IDs são diferentes: o MessageID de um e-mail do Postmark não existe na Postal. Durante a virada, guarde de qual provedor veio cada e-mail e continue recebendo os webhooks do Postmark para o que saiu por lá. Mande cada fluxo por um provedor só, para não duplicar nem pausar tudo por causa de e-mails errados.
Antes de virar a chave
- Domínio verificado na Postal, com os registros antigos ainda no lugar.
- Chave só de envio, restrita ao domínio, fora do código e do repositório.
- Decisão tomada sobre cada Message Stream: envio normal, campanha ou inbox.
- Supressões de todos os streams importadas, e descadastros de marketing importados como contatos descadastrados.
- Templates recriados com
{{{VARIÁVEL}}}, publicados, e a lógica de blocos movida para o código. - Metadados que vão em tags renomeados para letras, números,
_e-. - Webhook criado, assinatura conferida com o corpo cru e resposta 2xx em até 15 segundos.
- Testes feitos com
[email protected]e[email protected]. - Um fluxo por vez, olhando os bounces nos primeiros dias.
Perguntas comuns
A Postal tem Message Streams?
Não. A separação entre e-mail transacional e de marketing na Postal se faz com serviços diferentes (envio por API ou SMTP de um lado, campanhas e contatos de outro), com domínios separados e com tags. A lista de supressão e os webhooks são do time inteiro.
Posso manter o Postmark enquanto migro?
Pode, e é o mais seguro. Use as duas contas lado a lado e vire um fluxo de cada vez. Só tire os registros do Postmark quando nada mais sair por lá.
O histórico de eventos do Postmark vem junto?
Não. A Postal não importa e-mails nem eventos de outra plataforma, e guarda os seus por 30 dias. Exporte o que precisar antes de desligar o Postmark.
Migrar para a Postal
Outros guias
Cada plataforma tem as suas diferenças. Escolha o guia da que você usa hoje.
Vai migrar do Postmark?
Crie a sua conta e comece no plano Grátis. Para planejar a cota, o aquecimento e a ordem da migração, fale com a gente pelo formulário de contato com o domínio e o volume diário.