Novo: campanhas, automações e e-mails do produto na mesma conta. Conheça a Postal
Entrar Criar conta

Do Mailgun para a Postal, campo a campo

A API da Postal não é igual à do Mailgun: o envio troca form-data por JSON, a chave vai em outro cabeçalho e os webhooks têm outro formato. Este guia mostra o que tem correspondente, o que muda de lugar e o que fica sem par.

Formato do envio
Form-data para JSON
Autenticação
Basic para Bearer
Como virar o tráfego
Em etapas

Visão geral

No Mailgun você envia com POST /v3/{domínio}/messages, em form-data, com o usuário api e a sua chave. Na Postal o envio é POST /emails, em JSON, com Authorization: Bearer ps_…. A maior parte do que você usa tem correspondente. O resto exige uma decisão sua antes da virada, e este guia aponta onde.

MailgunPOST api.mailgun.net/v3/{domínio}/messagesform-data · usuário api
PostalPOST api.postal.so/emailsJSON · Authorization: Bearer

Muda só o formato

Remetente, destinatários, assunto, HTML, texto, anexos, cabeçalhos, tags, agendamento e SMTP têm correspondente. O que muda é o jeito de mandar: JSON no lugar de form-data e uma chave no cabeçalho Authorization.

Muda de lugar

Rastreamento, TLS obrigatório e região deixam de ser escolha de cada mensagem e passam a ser do domínio. Os descadastros do Mailgun passam a ser contatos descadastrados.

Não tem equivalente

Dados livres v: nos eventos, condições e laços nos templates, escolha da versão do template no envio, lista de exceções de bounces e enviar para o endereço de uma lista.

A Postal não imita a API do Mailgun. A única API com modo de compatibilidade é a da Resend: se você já usa o SDK dela, veja o guia da Resend. Para o Mailgun, a troca é no código, com fetch ou com os SDKs oficiais da Postal para Node.js e PHP.

1

Cadastre 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. Os registros usam nomes próprios, postal._domainkey e o subdomínio postal, então dá para publicá-los ao lado dos do Mailgun e só tirar os antigos depois da virada. Antes, confira que nenhum registro seu já usa esses nomes.

  • Região: o Mailgun tem duas URLs, US e EU. Na Postal a URL é uma só e a região é escolhida por domínio: São Paulo (padrão), us-east-1, us-east-2, eu-west-1 ou ap-northeast-1.
  • Para sistemas que só enviam, crie uma chave só de envio, restrita ao domínio. O token completo aparece uma única vez, na criação.
  • Se o domínio principal já envia pelo Mailgun, use um subdomínio dedicado na Postal.
2

Troque a chamada de envio

No Mailgun o domínio vai na URL e os campos vão em form-data. Na Postal o domínio vem do from e o corpo é JSON. A resposta é {"id": "…"}: guarde o ID, que volta nos webhooks como data.email_id e serve para consultar o e-mail em GET /emails/{id}. Com o cabeçalho Idempotency-Key, repetir uma requisição não duplica o envio.

curl · antes e depois
- curl -s --user 'api:$MAILGUN_API_KEY' \-   https://api.mailgun.net/v3/mg.exemplo.com.br/messages \-   -F from='Aurora Cosméticos <[email protected]>' \-   -F [email protected] \-   -F subject='Seu pedido #10482 saiu para entrega' \-   --form-string html='<p>O pedido <b>#10482</b> está a caminho.</p>' \-   -F o:tag='envio'+ 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": "category", "value": "shipping"}]+   }'
Node.js · antes e depois
- const form = new FormData();- form.append('from', 'Aurora Cosméticos <[email protected]>');- form.append('to', '[email protected]');- form.append('subject', 'Seu pedido #10482 saiu para entrega');- form.append('html', '<p>O pedido <b>#10482</b> está a caminho.</p>');- form.append('o:tag', 'envio');- const auth = Buffer.from('api:' + process.env.MAILGUN_API_KEY).toString('base64');- await fetch('https://api.mailgun.net/v3/mg.exemplo.com.br/messages', {-   method: 'POST',-   headers: { Authorization: `Basic ${auth}` },-   body: form,- });+ 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: 'category', value: 'shipping' }],+   }),+ });+ const { id } = await response.json();

Campo a campo

Tudo o que o envio do Mailgun aceita e o que acontece com cada parte na Postal.

Campos do envio: o que cada um vira.
MailgunNa PostalO que muda
URL e autenticaçãohttps://api.postal.so e Authorization: Bearer ps_…No Mailgun: https://api.mailgun.net/v3/{domínio}/messages (na região EU, api.eu.mailgun.net) e usuário api com a chave. Na Postal a URL é uma só e o domínio vem do from.
fromfromMesmo formato (Nome <email@dominio>). O domínio precisa estar verificado e com envio ativo.
totoTexto ou lista, até 50 endereços. Para mais gente, mande um item por pessoa em POST /emails/batch (até 100 por chamada).
cc, bcccc, bccTexto ou lista.
subject, text, htmlsubject, text, htmlIguais. Sem template, a Postal exige subject e ao menos html ou text; recomenda mandar os dois.
attachmentattachmentsNão há envio de arquivo em multipart. Mande filename e content em Base64, ou path com uma URL pública. Até 40 MB no total, e anexos não valem no lote.
inlineattachments com content_idNo HTML use <img src="cid:logo"> e, no anexo, "content_id": "logo".
o:tag (repetível, texto livre)tags: lista de {name, value}Nome e valor só aceitam letras ASCII, números, _ e -, até 256 caracteres. "September newsletter" vira september-newsletter. Até 50 por e-mail.
o:deliverytimescheduled_atData em ISO 8601, no futuro e a até 30 dias. Depende do plano e não vale no lote. Dá para reagendar (PATCH /emails/{id}) e cancelar (POST /emails/{id}/cancel).
o:tracking, o:tracking-opens, o:tracking-clicksopen_tracking e click_tracking do domínioNo Mailgun dá para ligar ou desligar em cada mensagem. Na Postal a escolha é do domínio e não existe campo no envio. Depende do plano.
o:testmodeEndereços de testeEm vez de uma marca no envio, você manda para [email protected] (e bounced@, complained@, suppressed@, delayed@). Não usa cota e gera webhooks reais. Depende do plano.
o:require-tlstls: "enforced" do domínioTambém é do domínio, não da mensagem.
h:X-Meu-CabecalhoheadersUm objeto: {"X-Pedido": "10482"}. Não dá para sobrescrever From, To, Subject, Message-ID e outros cabeçalhos de sistema.
v:minha-variavelSem campo igualO Mailgun devolve esses dados livres nos eventos (user-variables). Na Postal, use tags para valores curtos (voltam em data.tags) e guarde o resto no seu banco, ligado ao id do e-mail.
template e t:variablestemplate com id e variablesid aceita o ID ou o apelido (alias). Veja o passo de templates.
t:versionSem campo igualA Postal usa sempre a última versão publicada do template.
recipient-variablesUm item por pessoa no loteCada pessoa vira um objeto em POST /emails/batch, com o próprio template.variables.
Tamanho: 25 MB por mensagemAnexos: 40 MB no totalO Mailgun limita a mensagem inteira; a Postal limita os anexos. O seu plano pode permitir menos que 40 MB.
3

Recrie os templates

Os templates do Mailgun usam uma versão de Handlebars, com condições (if, unless), laços (each), with e equal. Os da Postal só substituem variáveis: {{{NOME}}}, com três chaves, e o valor precisa ser texto ou número. Não há condições, laços nem objetos aninhados. Quando o seu template depende de lógica, mova a lógica para o código: escolha entre dois 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.
  • No HTML os valores são escapados. Uma variável sem valor e sem valor padrão (fallback_value) devolve erro 422.
  • Declare as variáveis ao criar o template: até 50, com tipo texto ou número.
Variáveis do template
- <h1>Olá, {{name}}!</h1>- <p>Seu pedido: {{order}}</p>+ <h1>Olá, {{{NAME}}}!</h1>+ <p>Seu pedido: {{{ORDER}}}</p>
Envio com template
- -F template='pedido-enviado' \- -F t:variables='{"name": "Ana", "order": "10482"}'+ "template": {+   "id": "pedido-enviado",+   "variables": {"NAME": "Ana", "ORDER": "10482"}+ }
4

Aponte o SMTP para a Postal

Se algum sistema envia por SMTP, troque servidor, usuário e senha. O SMTP faz parte do plano. As mensagens passam pelas mesmas regras da API (domínio verificado, cotas, lista de supressão) e aparecem em E-mails e Logs.

  • Limites da Postal por mensagem: 40 MB e 50 destinatários. Quem passar disso recebe erro; divida o envio.
Nodemailer · antes e depois
- host: 'smtp.mailgun.org',- port: 587,- secure: false,- auth: { user: '[email protected]',-   pass: process.env.MAILGUN_SMTP_PASSWORD },+ host: 'smtp.postal.so',+ port: 465,+ secure: true,+ auth: { user: 'postal', pass: process.env.POSTAL_API_KEY },
MailgunPostal
Servidorsmtp.mailgun.org (região EU: smtp.eu.mailgun.org)smtp.postal.so
Portas25, 587 e 2525 com STARTTLS; 465 com TLS465 com TLS (recomendada) ou 587 com STARTTLS. Se a rede bloquear, 2465 (TLS) ou 2587 (STARTTLS).
UsuárioO login SMTP do domínio, um endereço completopostal
SenhaA senha SMTP criada para o domínioUma chave de API (ps_…). Uma chave só de envio basta.
Opções por cabeçalhoX-Mailgun-Tag, X-Mailgun-Variables, X-Mailgun-Track e X-Mailgun-Deliver-ByPelo SMTP não há tags, agendamento nem templates. Esses cabeçalhos seguiriam na mensagem como cabeçalhos comuns: tire-os.
5

Cadastre o webhook e troque a verificação

Crie o endpoint na Postal e guarde o signing_secret. A assinatura muda por completo: no Mailgun ela vem no corpo e usa uma chave de assinatura de webhooks; na Postal vem em cabeçalhos e usa o segredo de cada endpoint. Calcule 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.
Eventos de e-mail. A Postal também tem email.failed, email.suppressed, email.replied, email.received e outros.
Evento no MailgunEvento na PostalObservação
acceptedemail.sentO mais próximo, mas não idêntico: a Postal avisa quando entregou o e-mail ao provedor de envio.
deliveredemail.deliveredO servidor do destinatário aceitou a mensagem.
temporary_failemail.delivery_delayedA entrega foi adiada e novas tentativas acontecem sozinhas.
permanent_failemail.bouncedEm data.bounce.type, Permanent é o bounce que não deve ser repetido. O endereço entra na lista de supressão.
openedemail.openedPrecisa do rastreamento de aberturas ligado no domínio.
clickedemail.clickedPrecisa do rastreamento de cliques ligado no domínio.
complainedemail.complainedO endereço entra na lista de supressão.
unsubscribedcontact.updatedVale para campanhas: o contato descadastrado dispara contact.updated. O evento email.unsubscribed existe só no histórico do e-mail, não como webhook.
MailgunPostal
Onde se cadastraNa conta ou no domínioNo time, com POST /webhooks: URL HTTPS e a lista de eventos. A documentação não prevê filtro por domínio nem por tag: filtre no seu código.
FormatoJSON com signature e event-dataJSON com type, created_at e data
AssinaturaHMAC-SHA256 em hexadecimal de timestamp + token, com a chave de assinatura de webhooks. Os três campos vêm dentro do corpo.Padrão Standard Webhooks: cabeçalhos webhook-id, webhook-timestamp e webhook-signature. HMAC-SHA256 em Base64 de id.timestamp.corpo, com o whsec_… de cada endpoint.
Qual e-mail foievent-data.message.headers.message-iddata.email_id, o mesmo id devolvido no envio
Destinatárioevent-data.recipientdata.to, uma lista
Tagsevent-data.tags, uma lista de textosdata.tags, um objeto nome → valor
Dados v:event-data.user-variablesNão existe. Use as tags.
Motivo da falhareason, severity e delivery-statusdata.bounce (type, subType, message, diagnosticCode) e data.failed.reason
Resposta esperada200. A resposta 406 recusa o evento sem nova tentativa.Qualquer 2xx em até 15 segundos. Toda outra resposta conta como falha e gera nova tentativa; não há código que descarte o evento.
Novas tentativasPor cerca de 8 horas (5, 10 e 15 minutos, depois 1, 2 e 4 horas)8 tentativas em cerca de 27 horas. Reenvio pelo painel ou por POST /webhooks/{id}/events/{event_id}/replay.
Verificação · antes e depois
- const expected = crypto.createHmac('sha256', process.env.MAILGUN_SIGNING_KEY)-   .update(timestamp + token).digest('hex');- if (expected !== signature) return res.sendStatus(401);+ import { verifyWebhook } from 'postal-so';+ const event = verifyWebhook(req.body, req.headers, process.env.POSTAL_WEBHOOK_SECRET);
email.delivered · o que chega no seu servidor
{
  "type": "email.delivered",
  "created_at": "2026-10-24T13:02:14.120Z",
  "data": {
    "email_id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
    "from": "Aurora Cosméticos <[email protected]>",
    "to": ["[email protected]"],
    "subject": "Seu pedido #10482 saiu para entrega",
    "tags": { "category": "shipping" }
  }
}
200Responda 2xx em até 15 s
6

Leve a supressão, os descadastros e as listas

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.

No MailgunNa PostalComo levar
Bounces (/v3/{domínio}/bounces)Lista de supressãoPOST /suppressions/batch/add com emails, 100 por chamada.
Reclamações (/v3/{domínio}/complaints)Lista de supressãoO mesmo endpoint. A Postal registra sozinha as origens bounce e complaint dos envios que ela mesma faz; o que você adiciona entra como manual.
Descadastros (/v3/{domínio}/unsubscribes)Contatos com unsubscribed: trueUse POST /contacts/batch. Não ponha essas pessoas na lista de supressão: ela bloqueia todos os envios, inclusive os transacionais.
Lista de exceções (allowlist) de bouncesSem equivalente na documentaçãoSe você depende dela, fale com a gente antes de migrar.
Uma lista por domínioUma lista por timeUm endereço suprimido fica bloqueado em todos os domínios do time.
Remover um endereçoDELETE /suppressions/{id} ou POST /suppressions/batch/removeO {id} aceita o ID ou o próprio e-mail.
Lista de envio do MailgunNa PostalComo levar
A lista é um endereço (lista@dominio) e cada mensagem enviada a ele vai para os membros inscritosContatos, segmentos e tópicos. O envio para muita gente é uma campanha, não um endereço.Crie um segmento estático e envie uma campanha para ele. Não existe "enviar para o endereço da lista".
Membro: addressContato: emailNa Postal o e-mail é guardado em minúsculas e aparece uma vez por time.
Membro: namefirst_name e last_nameSepare o nome antes de importar.
Membro: subscribed: falseunsubscribed: trueQuem saiu da lista continua descadastrado.
Membro: vars (JSON livre)propertiesCada propriedade precisa ser criada antes e tem um tipo.
Até 1.000 membros por chamada (JSON)Até 10.000 contatos em POST /contacts/batchAté 1.000 são processados na hora. Acima disso o lote vai para a fila e a resposta traz o ID do lote.
7

Vire o tráfego aos poucos

Os IDs são diferentes: o ID de um e-mail do Mailgun não existe na Postal. Durante a virada, guarde de qual provedor veio cada e-mail e continue recebendo os webhooks do Mailgun 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.
  • Bounces e reclamações importados para a lista de supressão.
  • Descadastros importados como contatos com unsubscribed: true (se você faz marketing).
  • Templates recriados com {{{VARIÁVEL}}}, publicados, e a lógica movida para o código.
  • Tags renomeadas 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

Posso manter o Mailgun enquanto migro?

Pode, e é o mais seguro. Use as duas contas lado a lado, com os registros DNS das duas, e vire um fluxo de cada vez. Só tire os registros do Mailgun quando nada mais sair por lá.

O histórico de eventos do Mailgun 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 Mailgun.

Tenho vários domínios no Mailgun. Como fica?

Cadastre cada domínio na Postal, com a região de cada um (o número de domínios depende do plano). A lista de supressão e os webhooks são do time, não do domínio. Para separar o acesso, crie uma chave por domínio.

Vai migrar do Mailgun?

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.