Guias

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 /emails e 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 com o SDK da Resend

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

  1. 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 postal no DKIM).
  2. Crie uma chave de API (Autenticação). Para sistemas que só enviam, uma chave só de envio, restrita ao domínio.
  3. 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.
  4. Crie o webhook (veja o que muda) e confira a assinatura antes de virar o tráfego.
  5. 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.
  6. 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

PontoNa Postal
URL base e chavehttps://api.postal.so e Authorization: Bearer ps_…. Chaves de acesso total ou só de envio, opcionalmente restritas a um domínio.
Envio e respostaPOST /emails, devolve {"id"}. Igual.
Erros{"statusCode", "name", "message"}. Igual. Veja Erros.
Webhooks e assinaturaPadrã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 eventosOs 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 eventodata.email_id.
Motivo do bounce e da falhadata.bounce.message e data.failed.reason, com os mesmos campos.
Tentativas de webhook8 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.
Anexosfilename e content em base64, mais path (URL) e content_id para imagem embutida. Até 40 MB por e-mail.
IdempotênciaCabeçalho Idempotency-Key (1 a 256 caracteres, guardado por 24 horas) em POST /emails, /emails/batch e /events. Veja Idempotência.
Limite de requisiçõesPor 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 envioFree: 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çãoBounces 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 envioSão Paulo (sa-east-1, padrão), us-east-1, us-east-2, eu-west-1 e ap-northeast-1, por domínio.
CobrançaReais ou dólares. A lista pública de planos, preços e limites está em GET /plans.
RetençãoE-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-*:

Verificar a assinatura

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 id de 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 uma Idempotency-Key pró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 com POST /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.