Migrar da Resend
A API da Postal usa os mesmos recursos e formatos: veja o que continua igual, o que muda e como virar o tráfego com segurança.
#Visão geral
A API da Postal usa os mesmos recursos, nomes de campos e formatos de resposta da API da Resend, então a migração costuma ser trocar a URL base e a chave. Você pode manter o código e o SDK da Resend, ou passar para os SDKs da Postal (Node.js, PHP e Laravel) quando quiser.
- O que continua igual:
POST /emailse a resposta{"id"}, o envio em lote, agendamento e cancelamento, anexos, tags, templates com variáveis{{{VAR}}}, o formato de erro{statusCode, name, message}, a paginação por cursor, a chave de idempotência e os nomes dos eventos de webhook. - O que muda: a URL (
https://api.postal.so), a chave (ps_…), o segredo e as regras de assinatura dos webhooks (veja abaixo), os limites e cotas, e o que a Postal acrescenta: regiões incluindo São Paulo, cobrança em reais, inboxes, contatos, campanhas e automações.
#Continuar com o SDK da Resend
Os SDKs oficiais da Resend funcionam apontando para a Postal. No Node.js, passe baseUrl ao criar o cliente, ou defina RESEND_BASE_URL:
Node.js
import { Resend } from 'resend';
// Basta apontar o SDK da Resend para a Postal: a chave é a da Postal (ps_...).
const resend = new Resend(process.env.POSTAL_API_KEY, { baseUrl: 'https://api.postal.so' });
const { data, error } = await resend.emails.send({
from: 'Loja Exemplo <[email protected]>',
to: ['[email protected]'],
subject: 'Seu pedido #10482 saiu para entrega',
html: '<p>O pedido <b>#10482</b> está a caminho.</p>',
});
.env
# .env: sem mexer no código que já usa new Resend(chave)
RESEND_BASE_URL=https://api.postal.so
RESEND_API_KEY=ps_sua_chave
cURL
# Antes
curl -X POST 'https://api.resend.com/emails' -H 'Authorization: Bearer re_...' ...
# Depois: só a URL e a chave mudam
curl -X POST 'https://api.postal.so/emails' -H 'Authorization: Bearer ps_...' \
-H 'Content-Type: application/json' \
-d '{"from": "Loja Exemplo <[email protected]>", "to": ["[email protected]"], "subject": "Olá", "html": "<p>Olá!</p>"}'
Testado com o pacote resend 6.34 (Node.js): envio simples, agendado, em lote e com idempotência, consulta, edição e cancelamento de e-mails, anexos, domínios, chaves de API, webhooks e webhooks.verify. Outros SDKs que deixam trocar a URL base falam o mesmo formato, mas não foram testados um a um: se encontrar uma diferença, avise em [email protected].
#Passo a passo
- Cadastre e verifique o domínio na Postal (Domínios e DNS). Dá para manter o mesmo domínio na Resend e na Postal ao mesmo tempo: os registros são diferentes (a Postal usa o seletor
postalno DKIM). - Crie uma chave de API (Autenticação). Para sistemas que só enviam, uma chave só de envio, restrita ao domínio.
- Importe o que você já tem: a lista de supressão (
POST /suppressions/batch/add, 100 por requisição, para não escrever para quem já pediu para sair) e os contatos (importação por CSV). Recrie os templates (Templates): as variáveis{{{VAR}}}são as mesmas. - Crie o webhook (veja o que muda) e confira a assinatura antes de virar o tráfego.
- Troque a URL base e a chave no ambiente de testes, mande um e-mail para você mesmo e acompanhe em Logs e nos webhooks.
- Vire em produção e deixe a Resend por perto por alguns dias, só para os eventos dos e-mails que saíram por lá.
#O que muda de um para o outro
| Ponto | Na Postal |
|---|---|
| URL base e chave | https://api.postal.so e Authorization: Bearer ps_…. Chaves de acesso total ou só de envio, opcionalmente restritas a um domínio. |
| Envio e resposta | POST /emails, devolve {"id"}. Igual. |
| Erros | {"statusCode", "name", "message"}. Igual. Veja Erros. |
| Webhooks e assinatura | Padrão Standard Webhooks: cabeçalhos webhook-id, webhook-timestamp e webhook-signature, e os mesmos valores em svix-id, svix-timestamp e svix-signature, para bibliotecas que leem os nomes da Svix. O segredo é o signing_secret (whsec_…) de cada endpoint. |
| Nomes dos eventos | Os mesmos (email.delivered, email.bounced…), mais email.replied, contact.*, form.submitted, broadcast.sent e sending.paused / sending.resumed (aviso na hora em que os envios do time são pausados). |
| Campo do e-mail no evento | data.email_id. |
| Motivo do bounce e da falha | data.bounce.message e data.failed.reason, com os mesmos campos. |
| Tentativas de webhook | 8 tentativas em cerca de 27 horas (imediato, 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h), sempre com o mesmo webhook-id; reenvio manual pelo painel ou por POST /webhooks/{id}/events/{event_id}/replay. |
| Anexos | filename e content em base64, mais path (URL) e content_id para imagem embutida. Até 40 MB por e-mail. |
| Idempotência | Cabeçalho Idempotency-Key (1 a 256 caracteres, guardado por 24 horas) em POST /emails, /emails/batch e /events. Veja Idempotência. |
| Limite de requisições | Por time, somando todas as chaves: 5 (Free), 10 (Pro), 20 (Scale) e 50 (Enterprise) por segundo, com X-RateLimit-* em toda resposta. Veja Limites. |
| Cotas de envio | Free: 100 por dia e 3.000 por mês. Pro: 50.000 por mês. Scale: 100.000 por mês. O dia zera às 00:00 UTC. Ao atingir: 429 daily_quota_exceeded ou monthly_quota_exceeded (no SMTP, 452 4.5.3). |
| Pausa por reputação | Bounces permanentes acima de 5% ou reclamações acima de 0,3% (últimos 7 dias, a partir de 100 envios) pausam os envios do time: 403 sending_paused. Antes disso, donos e admins recebem um aviso por e-mail. GET /usage mostra sending.paused. |
| Regiões de envio | São Paulo (sa-east-1, padrão), us-east-1, us-east-2, eu-west-1 e ap-northeast-1, por domínio. |
| Cobrança | Reais ou dólares. A lista pública de planos, preços e limites está em GET /plans. |
| Retenção | E-mails, eventos de webhook e logs ficam 30 dias; os eventos enviados por POST /events, 90. |
#Webhooks
Na Postal, cada endpoint tem o seu signing_secret. A assinatura é um HMAC-SHA256 do texto {webhook-id}.{webhook-timestamp}.{body}, no padrão Standard Webhooks, e o resend.webhooks.verify aceita os cabeçalhos webhook-* e svix-*:
Node.js (SDK da Resend)
import { Resend } from 'resend';
const resend = new Resend(process.env.POSTAL_API_KEY, { baseUrl: 'https://api.postal.so' });
// Lê o corpo cru da requisição, sem reformatar o JSON
const event = resend.webhooks.verify({
payload: rawBody,
headers: {
id: request.headers['webhook-id'],
timestamp: request.headers['webhook-timestamp'],
signature: request.headers['webhook-signature'],
},
webhookSecret: process.env.POSTAL_WEBHOOK_SECRET, // whsec_...
});
Outras linguagens: veja a verificação da assinatura, com exemplos, ou use o SDK da Postal. O evento traz só o resumo do e-mail; para ler um e-mail recebido, use GET /emails/receiving/{email_id}.
#Convivendo com as duas durante a virada
- IDs diferentes. Os IDs dos e-mails e dos webhooks da Resend não existem na Postal (e o contrário). Quem guarda o
idde cada e-mail para mostrar o status deve guardar também de qual provedor ele veio, e continuar consultando e recebendo webhooks da Resend para os e-mails que saíram por lá. - Marque a origem. Use
tags(por exemplo,provider: postal) nos e-mails novos e umaIdempotency-Keyprópria em cada envio: ela vale só para a Postal. - Um provedor por vez para o mesmo fluxo. Convites, redefinições de senha e códigos devem sair por um só caminho, para não duplicar nem pausar tudo por causa de e-mails errados: confira os bounces nos primeiros dias, porque o limite de reputação vale para o time inteiro.
- Endereços que voltaram a funcionar. Um bounce permanente põe o endereço na lista de supressão. Se a pessoa corrigiu o e-mail, remova com
DELETE /suppressions/{id}ou em lote comPOST /suppressions/batch/remove.
#O que a Postal acrescenta
- Dados no Brasil e envio a partir de São Paulo.
- Inboxes para receber e responder e-mails, com conversas, pastas, etiquetas, rascunhos, tradução e um agente de IA (Recebidos e inboxes).
- Audiência e marketing: contatos, segmentos, tópicos, formulários, campanhas e automações por eventos, com a mesma chave.
- Integrações com Stripe, Supabase, Clerk, Auth0 e Firebase (Integre seu app) e a CLI
postal.
Precisa de ajuda na virada?
Escreva para [email protected] com o domínio e o volume diário: ajudamos a planejar a cota, o aquecimento e a ordem da migração.