Migrar do SendGrid para a Postal
O SendGrid e a Postal enviam o mesmo e-mail, mas a chamada tem outro formato: no SendGrid, destinatários, variáveis e argumentos ficam dentro de personalizations; na Postal, são campos do próprio e-mail. Este guia compara campo a campo e diz o que não tem equivalente.
- Muda no código: envio e webhooks
- Reescrever
- Dá para rodar as duas juntas
- Sim
- Tempo estimado, conforme o que você usa
- Horas a dias
- Tipo de envio, mais marketing
- API e SMTP
Verifique o domínio na Postal
Cadastre o domínio no painel e crie os registros que ele mostra. Pelos nomes, os registros da Postal não colidem com os do SendGrid, e os dois ficam verificados ao mesmo tempo durante a virada. Se você já tem um _dmarc, mantenha o seu.
- A Postal confere os registros sozinha por até 72 horas e avisa por webhook (
domain.updated) - Crie uma chave de API (ela começa com
ps_). Para sistemas que só enviam, use uma chave só de envio, restrita ao domínio
| Registro | SendGrid (segurança automática) | Postal |
|---|---|---|
| DKIM | CNAME em s1._domainkey e s2._domainkey | TXT em postal._domainkey |
| Return-Path e SPF | CNAME em em e um número (por exemplo, em1234) | MX e TXT em postal |
| DMARC | TXT em _dmarc | Opcional. O mesmo _dmarc vale para os dois |
Reescreva a chamada de envio
A Postal não aceita o formato /v3/mail/send: a chamada passa a ser POST /emails, com os campos no primeiro nível. Abaixo, o mesmo e-mail nos dois formatos. Quem usa o SDK do SendGrid troca o envio pelo SDK da Postal (Node.js, PHP e Laravel) ou por HTTP, que é JSON com a chave no cabeçalho Authorization.
- O SendGrid responde 202 Accepted; a Postal responde 200 com o
iddo e-mail. Guarde oid: é ele que aparece nos webhooks e emGET /emails/{id} - Vários destinatários no mesmo
toformam um único e-mail em que todos se veem. Para um e-mail por pessoa, use o envio em lote - Repetir a chamada com a mesma
Idempotency-Keynão duplica o envio
Antes: SendGrid
POST https://api.sendgrid.com/v3/mail/send
Authorization: Bearer <chave do SendGrid>
{
"personalizations": [{
"to": [{ "email": "[email protected]", "name": "Ana" }],
"custom_args": { "order_id": "10482" }
}],
"from": { "email": "[email protected]", "name": "Loja Exemplo" },
"subject": "Seu pedido #10482 saiu para entrega",
"content": [
{ "type": "text/plain", "value": "O pedido #10482 está a caminho." },
{ "type": "text/html", "value": "<p>O pedido <b>#10482</b> está a caminho.</p>" }
],
"categories": ["entrega"]
}Depois: Postal
POST https://api.postal.so/emails
Authorization: Bearer ps_sua_chave
Idempotency-Key: pedido-10482-saiu
{
"from": "Loja Exemplo <[email protected]>",
"to": ["Ana <[email protected]>"],
"subject": "Seu pedido #10482 saiu para entrega",
"text": "O pedido #10482 está a caminho.",
"html": "<p>O pedido <b>#10482</b> está a caminho.</p>",
"tags": [
{ "name": "category", "value": "entrega" },
{ "name": "order_id", "value": "10482" }
]
}No código
- const sgMail = require('@sendgrid/mail');- sgMail.setApiKey(process.env.SENDGRID_API_KEY);- await sgMail.send({- to: '[email protected]',- from: '[email protected]',- subject, html,- });+ import { Postal } from 'postal-so';+ const postal = new Postal(); // lê POSTAL_API_KEY+ await postal.emails.send({+ from: 'Loja Exemplo <[email protected]>',+ to: '[email protected]',+ subject, html,+ });Mapeie os campos
A tabela vale para o código novo e como lista de conferência do que o seu sistema usa hoje. As linhas que mudam o jeito de montar o e-mail são as de destinatários em lote, categorias e argumentos personalizados.
| No SendGrid | Na Postal | Observação |
|---|---|---|
POST /v3/mail/send | POST /emails | A chave vai em Authorization: Bearer nos dois. Na Postal ela começa com ps_. |
personalizations[].to, lista de {email, name} | to: texto ou lista de até 50, no formato Nome <email> | Uma lista é um único e-mail, em que todos se veem. |
Várias personalizations (um e-mail por item) | POST /emails/batch: lista de até 100 e-mails, cada um com o seu to | No SendGrid o limite é 1.000 personalizations por requisição. O lote é um recurso do plano e é validado antes: ou todos entram na fila, ou nenhum. |
cc e bcc | cc e bcc | Mesmo comportamento: o bcc não aparece para os outros. |
from: {email, name} | from: texto Nome <email> | O domínio precisa estar verificado (e ser o da chave, se ela for restrita). |
reply_to e reply_to_list | reply_to: texto ou lista | |
subject, global ou por personalização | subject | Assunto diferente por pessoa: um item por e-mail no lote, ou um template com variável no assunto. |
content: lista de {type, value} | html e text | Campos separados. Sem text, o html é obrigatório; mande os dois quando puder. |
attachments: content, filename, type, disposition, content_id | attachments: content, filename, content_type, content_id, e path (URL) | type vira content_type. Imagem embutida: content_id e cid: no HTML. Não existe disposition. Anexos até 40 MB no total, não valem no lote e dependem do plano. |
template_id e dynamic_template_data | template: {id, variables} | Veja “O que não tem equivalente”: os templates da Postal só substituem variáveis. |
headers | headers | Cada plataforma tem a sua lista de cabeçalhos que não podem ser trocados. Na Postal: From, To, Cc, Bcc, Subject, Reply-To, Sender, Date, Message-ID, Return-Path, MIME-Version, Content-Type, Content-Transfer-Encoding e DKIM-Signature. |
categories (até 10) | tags: [{name, value}] (até 50) | Nome e valor só aceitam letras ASCII, números, _ e - (até 256 caracteres): “Promoção de Natal” vira promocao-de-natal. |
custom_args | tags | Mesma regra de caracteres. Nos webhooks, as tags vêm em data.tags ({"order_id": "10482"}), e não como chaves soltas no evento. Para dados livres, guarde no seu banco junto com o id do e-mail. |
send_at, Unix, até 72 horas | scheduled_at, ISO 8601, até 30 dias | Exemplo: 2026-10-25T09:00:00-03:00. Não vale no lote e depende do plano. Dá para reagendar (PATCH /emails/{id}) e cancelar (POST /emails/{id}/cancel). |
mail_settings.sandbox_mode | Endereços de teste: [email protected] e outros | Em vez de um modo da requisição, você troca o destinatário. O e-mail é simulado, não usa cota e gera webhooks reais. |
tracking_settings.open_tracking e click_tracking | open_tracking e click_tracking no domínio | Ligados por domínio (painel ou PATCH /domains/{id}), não por e-mail. Dependem do plano. |
Se você usa SMTP, troque a conexão
Quem manda por SMTP não reescreve nada: troca o servidor, o usuário e a senha. O usuário fixo é postal, e não apikey. A Postal também aceita 2465 (TLS) e 2587 (STARTTLS) para redes que bloqueiam as portas comuns.
- O cabeçalho
X-SMTPAPI(category,unique_args,sub,send_at,filters…) não é interpretado pela Postal: ele segue como um cabeçalho comum. O que você usa dele precisa ir para a API - Os destinatários são os do envelope (
RCPT TO), até 50 por mensagem. Quem não aparece em To nem em Cc é tratado como Bcc - O SMTP da Postal não tem tags, agendamento nem templates. Cotas, lista de supressão, eventos e webhooks são os mesmos da API
- Mensagens até 40 MB; a Postal responde
250 2.0.0 OK queued as <id>, e o id é o do e-mail na API
- Servidor: smtp.sendgrid.net- Porta: 587 (ou 25, 2525, 465)- Usuário: apikey- Senha: a chave de API do SendGrid+ Servidor: smtp.postal.so+ Porta: 465 (TLS direto) ou 587 (STARTTLS)+ Usuário: postal+ Senha: a chave de API da Postal (ps_…)Recrie os webhooks
O SendGrid manda o Event Webhook assinado com uma chave pública (ECDSA); a Postal assina com um segredo por endpoint, no padrão Standard Webhooks. O corpo também muda: um evento por requisição, com o e-mail dentro de data.
| Evento do SendGrid | Evento da Postal | Observação |
|---|---|---|
processed | email.sent | Aproximado: a Postal avisa quando entregou o e-mail ao provedor de envio. |
delivered | email.delivered | O servidor do destinatário aceitou a mensagem. |
deferred | email.delivery_delayed | A entrega foi adiada e novas tentativas acontecem sozinhas. |
bounce | email.bounced | data.bounce traz type (Permanent, Transient ou Undetermined) e message. Bounce permanente põe o endereço na lista de supressão. |
dropped | email.suppressed ou email.failed | Aproximado: suppressed quando o destinatário estava na lista de supressão; failed, com failed.reason, quando não foi possível enviar. |
open | email.opened | Só com o rastreamento de aberturas ligado no domínio. |
click | email.clicked | data.click traz link, ipAddress, userAgent e timestamp. Só com o rastreamento de cliques ligado. |
spamreport | email.complained | O endereço vai para a lista de supressão. |
unsubscribe | contact.updated | O descadastro de um contato de marketing gera contact.updated. |
group_unsubscribe e group_resubscribe | sem evento próprio | Os tópicos da Postal não têm um evento com esse nome. |
| No SendGrid | Na Postal | Observação |
|---|---|---|
| Um POST com a lista de eventos | Um POST por evento: {type, created_at, data} | Os exemplos do SendGrid mostram um array de eventos. Se o seu código percorre uma lista, ele passa a receber um objeto. |
sg_message_id, email, timestamp | data.email_id, data.to, created_at | O email_id é o id devolvido por POST /emails. O horário vem em ISO 8601, e não em Unix. |
Assinatura ECDSA: X-Twilio-Email-Event-Webhook-Signature e -Timestamp | Standard Webhooks, HMAC-SHA256: webhook-id, webhook-timestamp e webhook-signature | Troque a verificação pelo verifyWebhook do SDK ou pelo exemplo da documentação. O segredo (whsec_…) é de cada endpoint. A documentação de webhooks da Postal não tem o OAuth 2.0 opcional do SendGrid. |
| Novas tentativas por até 24 horas, resposta 2xx | 8 tentativas em cerca de 27 horas, resposta 2xx em até 15 segundos | Todas com o mesmo webhook-id: use para descartar duplicatas. Reenvio manual pelo painel ou por POST /webhooks/{id}/events/{event_id}/replay. |
| Event Webhook em Settings, Mail Settings | Webhooks, no painel, ou POST /webhooks | A URL precisa ser HTTPS e pública. Você escolhe os eventos de cada endpoint. |
import express from 'express';
import { verifyWebhook, PostalWebhookVerificationError } from 'postal-so';
const app = express();
// O corpo precisa chegar cru (Buffer) para a assinatura bater.
app.post('/webhooks/postal', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = verifyWebhook(req.body, req.headers, process.env.POSTAL_WEBHOOK_SECRET);
if (event.type === 'email.bounced') {
// descarte o endereço
}
res.sendStatus(200);
} catch (error) {
if (error instanceof PostalWebhookVerificationError) {
return res.sendStatus(400);
}
throw error;
}
});Leve as supressões e os contatos
Primeiro a lista de supressão, para não escrever a quem não deve receber. Depois os contatos, se você usa a audiência de marketing. O SendGrid exporta cada tipo de supressão por uma rota própria; a Postal tem uma lista só.
- Faça a importação de contatos depois da supressão: endereços que já estão na lista de supressão são ignorados e não usam o limite de contatos do plano
- Campanhas e automações do marketing precisam ser recriadas na Postal; o que você leva são os contatos e o conteúdo
curl -X POST https://api.postal.so/suppressions/batch/add \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"emails": ["[email protected]", "[email protected]"]}'| No SendGrid | Na Postal | Observação |
|---|---|---|
Bounces: GET /v3/suppression/bounces, até 500 por página (limit, offset) | POST /suppressions/batch/add | Leia o email de cada item e envie 100 por requisição. Endereços que voltarem a dar bounce na Postal entram sozinhos. |
Reclamações de spam: GET /v3/suppression/spam_reports | POST /suppressions/batch/add | Quem reclamou não deve receber de novo: leve também. |
Blocks: GET /v3/suppression/blocks | Não importe | No SendGrid, o bloqueio vem de um problema na mensagem ou no IP de envio e, por padrão, não suprime envios futuros. A Postal não tem uma lista equivalente. |
Descadastros globais: GET /v3/suppression/unsubscribes | Contatos com unsubscribed: true (coluna de descadastro no CSV) | Quem saiu só do marketing fica na audiência sem receber campanhas. Se a pessoa não deve receber nada, nem e-mails do sistema, ponha o endereço na lista de supressão: ele não vira contato. |
| Grupos de descadastro (ASM) | Tópicos, com escolha de cada contato (opt_in ou opt_out) | A importação em CSV pode inscrever todos os contatos importados em tópicos. |
Contatos: PUT /v3/marketing/contacts, até 30.000 por requisição | POST /contacts/batch, até 10.000 | Acima de 1.000 itens o lote vai para a fila (202) e você acompanha em GET /contacts/batches/{id}. O lote cria ou atualiza. |
Importar CSV: PUT /v3/marketing/contacts/imports | Importar CSV no painel, ou POST /contacts/imports | Arquivo de até 20 MB. Quem já existe é atualizado pelo e-mail. |
Exportar: POST /v3/marketing/contacts/exports (CSV ou JSON) | Exporte em CSV e importe | A exportação do SendGrid é assíncrona: baixe os arquivos e use o CSV na importação. |
| Listas | Segmentos estáticos | Na importação, escolha o segmento. Só segmentos estáticos recebem contatos importados. |
| Campos personalizados | Propriedades de contato | Crie as propriedades antes de importar, porque a importação não cria propriedades novas. |
Rode as duas lado a lado e vire
Com o domínio verificado nos dois, passe um fluxo por vez. Convites, redefinições de senha e códigos devem sair por um só caminho de cada vez.
- Os IDs são de cada plataforma: guarde de qual provedor cada e-mail saiu e continue recebendo eventos do SendGrid para o que saiu por lá
- Use
tags(por exemplo,provider: postal) para separar os e-mails novos nos webhooks - Contas novas começam com um teto de 500 e-mails por dia, que cresce com a reputação da conta, e os envios pausam com bounces permanentes acima de 3% a 5%, conforme o nível, ou reclamações acima de 0,3%: suba o volume por etapas
O que não tem equivalente
| No SendGrid | O que fazer na Postal |
|---|---|
Templates dinâmicos com Handlebars ({{#if}}, {{#each}}, dynamic_template_data com listas) | Os templates da Postal só substituem variáveis {{{NOME}}} (texto ou número; no HTML, os valores são escapados). Resolva condicionais e listas no seu sistema e envie o HTML pronto em html. |
ip_pool_name | Não há campo equivalente na API de envio. |
batch_id (cancelar ou pausar um grupo de envios) | Cancele cada e-mail agendado com POST /emails/{id}/cancel. |
asm.group_id no envio | O envio por POST /emails não aceita tópico. Para pedir o descadastro em e-mails do sistema, use o cabeçalho List-Unsubscribe em headers. |
footer, subscription_tracking e ganalytics por e-mail | Sem equivalente na API de envio: monte o rodapé e os links com UTM no HTML. Os e-mails de campanha da Postal já levam o link de descadastro. |
bypass_list_management e demais bypass_* | Não há como ignorar a lista de supressão: destinatários suprimidos são removidos do envio e geram email.suppressed. |
| Rastreamento de aberturas e cliques por e-mail | Vale por domínio, não por mensagem. |
disposition de anexos | Não existe. Imagem embutida usa content_id. |
X-SMTPAPI | Não é interpretado. Use a API para tags, agendamento e variáveis. |
| Webhooks com OAuth 2.0 | Não consta da documentação de webhooks da Postal; a proteção é a assinatura. |
Antes de virar a chave
- Domínio verificado na Postal, chave de API criada e guardada no ambiente
- Chamada de envio reescrita e testada com um endereço de teste
- Categorias e argumentos personalizados convertidos em tags (só letras ASCII, números, _ e -)
- Modelos com Handlebars resolvidos no seu sistema ou recriados com variáveis simples
- Webhooks recriados, assinatura conferida e código que lia uma lista de eventos ajustado
- Supressões e contatos importados, nessa ordem
- O seu código guarda de qual provedor cada e-mail saiu
- Bounces conferidos nos primeiros dias depois da virada
Perguntas frequentes
Posso continuar usando o @sendgrid/mail?
Não para enviar pela Postal: a Postal não aceita o formato /v3/mail/send. Troque o envio pelo SDK da Postal ou por uma chamada HTTP a POST /emails.
Preciso reescrever todos os meus modelos?
Só os que usam condicionais ou repetições do Handlebars. Modelos que só trocam nomes e números viram templates com {{{VARIAVEL}}}. Nos envios por API, a Postal só substitui variáveis.
O meu sistema manda por SMTP. O que muda?
O servidor (smtp.postal.so), o usuário (postal) e a senha (uma chave de API da Postal). Se ele usa o cabeçalho X-SMTPAPI, essas funções precisam ir para a API.
Posso manter o domínio autenticado no SendGrid?
Sim. Os registros de DNS têm nomes diferentes, então os dois ficam verificados ao mesmo tempo durante a virada.
Continue por aqui
Outros guias
Cada plataforma tem o seu: o passo a passo geral está em Migrar.
Pronto para reescrever a chamada?
Crie a sua conta e comece no plano Grátis. Para planejar a cota e a ordem da migração, fale com a gente pelo formulário de contato.