Paulo CastellanoComo lidar com bounces de e-mail programaticamente
Construa tratamento adequado de bounces com webhooks, listas de supressão e lógica de retry pra proteger sua reputação de remetente.

Todo e-mail que você manda pra um endereço morto é um sinal pros provedores de e-mail de que você não sabe o que está fazendo. Sinais demais e seus e-mails param de alcançar gente real. Tratamento de bounces não é um nice-to-have. É infraestrutura que protege sua capacidade de enviar e-mail.
Este guia mostra como construir tratamento programático de bounces com a email API da Sendkit: endpoints de webhook, listas de supressão, lógica de retry e processamento de reclamações. No final você vai ter um sistema que mantém sua taxa de bounce bem abaixo dos limites que importam.
O que são bounces de e-mail
Um bounce acontece quando um e-mail não pode ser entregue ao destinatário. O servidor receptor devolve uma rejeição, e seu provedor de e-mail a captura. Existem dois tipos.
Hard bounces são falhas permanentes. O endereço de e-mail não existe, o domínio é inválido ou o servidor receptor bloqueou a entrega explicitamente. Códigos SMTP comuns: 550 (caixa não encontrada), 551 (usuário não é local), 552 (caixa cheia, às vezes classificado como hard). Uma vez que você recebe um hard bounce, aquele endereço é morto pra você. Não envie mais.
Soft bounces são falhas temporárias. A caixa está cheia, o servidor caiu temporariamente, bateu um rate limit ou tem um problema de DNS no lado receptor. Códigos SMTP na faixa 4xx. Esses podem se resolver sozinhos, então tentar de novo faz sentido, mas só até certo ponto.
A distinção importa porque sua lógica de tratamento deveria ser completamente diferente pra cada tipo.
Por que bounces danificam a reputação do remetente
Provedores de e-mail rastreiam sua taxa de bounce como sinal central pra reputação de remetente. Google e Yahoo aplicaram um teto rígido em 2024: se sua taxa de bounce ultrapassa 2%, você começa a ser limitado ou bloqueado. A Microsoft seguiu com aplicação parecida no Outlook.com e Hotmail em 2025.
A matemática é simples. Se você envia 10.000 e-mails e 300 dão bounce, você está em 3%. Já é o bastante pro Gmail começar a rotear seu e-mail pra spam pra todo mundo da sua lista, incluindo os endereços válidos. Sua entregabilidade despenca, as taxas de abertura caem e você fica em um buraco que leva semanas pra sair.
A correção não é complicada: pare de enviar pra endereços que dão bounce, e valide endereços antes que entrem na sua lista. Mas você precisa do encanamento pra fazer isso acontecer automaticamente.
Pra uma análise mais profunda de todos os fatores que afetam a entrega na caixa de entrada, leia nosso guia sobre como melhorar a entregabilidade de e-mail.
Configurando um endpoint de webhook pra eventos de bounce
A Sendkit envia eventos via webhook em tempo real quando algo acontece com um e-mail que você mandou. Os eventos relevantes pra tratamento de bounces são:
email.bounced— o e-mail deu bounce (hard ou soft)email.complained— o destinatário marcou seu e-mail como spamemail.delivered— confirmação de entrega bem-sucedidaemail.failed— o e-mail falhou completamente no envio
Você configura endpoints de webhook no dashboard da Sendkit ou via API. A Sendkit assina cada payload de webhook com HMAC-SHA256, enviado no header x-sendkit-signature, pra você poder verificar que a requisição é legítima.

Construindo o handler do webhook
Aqui vai um handler de webhook completo em Express.js que verifica a assinatura e roteia eventos pra lógica de processamento certa.
import express from 'express';
import crypto from 'crypto';
import { Sendkit } from '@sendkitdev/sdk';
const app = express();
const sendkit = new Sendkit('sk_your_api_key');
const WEBHOOK_SECRET = process.env.SENDKIT_WEBHOOK_SECRET;
app.post('/webhooks/sendkit', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-sendkit-signature'] as string;
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
switch (event.type) {
case 'email.bounced':
handleBounce(event.data);
break;
case 'email.complained':
handleComplaint(event.data);
break;
case 'email.failed':
handleFailure(event.data);
break;
}
res.status(200).json({ received: true });
});Sempre verifique a assinatura antes de processar qualquer payload de webhook. Sem verificação, qualquer um pode enviar eventos de bounce falsos pro seu endpoint e corromper sua lista de supressão.
Processando hard bounces
Hard bounces são inegociáveis. O endereço está morto. Suprima imediatamente e nunca mais envie.
const handleBounce = async (data: {
recipient: string;
bounce_type: string;
error_code: string;
message: string;
}) => {
if (data.bounce_type === 'hard') {
await db.suppressionList.upsert({
where: { email: data.recipient },
create: {
email: data.recipient,
reason: 'hard_bounce',
errorCode: data.error_code,
suppressedAt: new Date(),
},
update: {
reason: 'hard_bounce',
errorCode: data.error_code,
suppressedAt: new Date(),
},
});
await db.contact.update({
where: { email: data.recipient },
data: { status: 'suppressed' },
});
console.log(`Hard bounce: suppressed ${data.recipient}`);
return;
}
if (data.bounce_type === 'soft') {
await handleSoftBounce(data);
}
};Sem retry. Sem segunda chance. Um hard bounce significa que a caixa não existe. Tentar de novo só aumenta sua taxa de bounce e danifica mais a reputação.
Processando soft bounces com backoff exponencial
Soft bounces merecem retries, mas com limites. Tente algumas vezes com delays crescentes. Se o endereço continuar falhando, trate como hard bounce.
const MAX_SOFT_BOUNCES = 3;
const BACKOFF_DELAYS = [60_000, 300_000, 3_600_000]; // 1min, 5min, 1hr
const handleSoftBounce = async (data: { recipient: string; error_code: string }) => {
const record = await db.softBounce.findUnique({
where: { email: data.recipient },
});
const bounceCount = (record?.count ?? 0) + 1;
if (bounceCount >= MAX_SOFT_BOUNCES) {
await db.suppressionList.create({
data: {
email: data.recipient,
reason: 'repeated_soft_bounce',
errorCode: data.error_code,
suppressedAt: new Date(),
},
});
console.log(`Soft bounce limit reached: suppressed ${data.recipient}`);
return;
}
await db.softBounce.upsert({
where: { email: data.recipient },
create: { email: data.recipient, count: 1, lastBouncedAt: new Date() },
update: { count: bounceCount, lastBouncedAt: new Date() },
});
const delay = BACKOFF_DELAYS[bounceCount - 1];
setTimeout(async () => {
await sendkit.emails.send({
from: '[email protected]',
to: data.recipient,
subject: 'Your pending notification',
html: '<p>Retry attempt</p>',
});
}, delay);
};Três strikes é um limite razoável. Depois de três soft bounces, o endereço está ou permanentemente quebrado ou consistentemente instável. De qualquer forma, você não quer ele na sua lista.

Construindo uma lista de supressão
Sua lista de supressão é uma tabela no banco que você checa antes de cada envio. Sem exceção. O schema é direto:
CREATE TABLE suppression_list (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
reason VARCHAR(50) NOT NULL, -- hard_bounce, repeated_soft_bounce, complaint
error_code VARCHAR(20),
suppressed_at TIMESTAMP DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_suppression_email ON suppression_list (email);Antes de cada envio, cheque a lista:
const sendEmail = async (to: string, subject: string, html: string) => {
const suppressed = await db.suppressionList.findUnique({
where: { email: to },
});
if (suppressed) {
console.log(`Skipping suppressed address: ${to} (${suppressed.reason})`);
return { skipped: true, reason: suppressed.reason };
}
return sendkit.emails.send({
from: '[email protected]',
to,
subject,
html,
});
};Pra envios em massa, consulte a lista de supressão em batch em vez de um por um. Puxe todos os endereços suprimidos da sua lista de destinatários em uma única query e filtre antes de começar a enviar.
Se você está gerenciando contatos pela Sendkit, pode sincronizar o status de supressão pra manter tudo consistente na sua infraestrutura de envio.
Supressão automática da Sendkit
Boa notícia: a Sendkit mantém sua própria lista de supressão automaticamente. Quando um hard bounce volta, a Sendkit suprime aquele endereço em toda sua conta. Você não precisa construir nada do que está acima pra ter proteção básica.
Então por que construir o seu? Três motivos.
Lógica customizada. Você pode querer limites diferentes pra tipos diferentes de e-mail. E-mails transacionais (reset de senha, confirmações de pedido) podem justificar retries mais agressivos que e-mails de marketing. Sua própria camada de supressão te deixa tomar essas decisões.
Sincronização entre sistemas. Se você envia de múltiplos serviços ou tem endereços em um CRM, seus dados de supressão precisam se propagar por todo lado. Seu próprio banco é a fonte da verdade.
Trilha de auditoria. Quando alguém pergunta por que parou de receber e-mails, você pode mostrar exatamente quando a supressão aconteceu, o que causou e qual evento de bounce disparou.
Pense na supressão automática da Sendkit como a rede de segurança e sua lógica customizada como a camada refinada em cima.
Monitorando taxas de bounce
Você precisa de visibilidade sobre sua taxa de bounce em cada envio. Configure uma função simples de monitoramento:
const trackBounceRate = async (campaignId: string) => {
const stats = await db.emailEvent.groupBy({
by: ['event_type'],
where: { campaignId },
_count: true,
});
const total = stats.reduce((sum, s) => sum + s._count, 0);
const bounces = stats.find(s => s.event_type === 'email.bounced')?._count ?? 0;
const bounceRate = (bounces / total) * 100;
if (bounceRate > 1.5) {
// Alert before hitting the 2% threshold
await alertOps(`Campaign ${campaignId} bounce rate at ${bounceRate.toFixed(2)}%`);
}
return { total, bounces, bounceRate };
};Defina seu threshold de alerta em 1,5%, não 2%. Você quer pegar problemas antes que disparem aplicação do Google, Yahoo ou Microsoft. Quando você chega em 2%, o dano já está sendo feito.
A melhor forma de manter taxas de bounce baixas é evitar que endereços ruins entrem na sua lista. Rode validação de e-mail no ponto de coleta e periodicamente contra sua lista existente. Explicamos como no nosso guia sobre validar endereços de e-mail antes de enviar.
Lidando com reclamações
Eventos de reclamação (email.complained) disparam quando um destinatário clica em "Denunciar Spam" no cliente de e-mail. Isso é pior que um bounce. Um bounce significa que o endereço é ruim. Uma reclamação significa que uma pessoa real ativamente não quer seu e-mail.
const handleComplaint = async (data: { recipient: string }) => {
await db.suppressionList.upsert({
where: { email: data.recipient },
create: {
email: data.recipient,
reason: 'complaint',
suppressedAt: new Date(),
},
update: {
reason: 'complaint',
suppressedAt: new Date(),
},
});
await db.contact.update({
where: { email: data.recipient },
data: { status: 'unsubscribed', unsubscribeReason: 'spam_complaint' },
});
};Suprima imediatamente. Não envie um e-mail "que pena ver você ir". Não coloque num fluxo de re-engajamento. Te reportaram como spam. A única resposta correta é parar de enviar.
As ferramentas do postmaster do Google mostram taxas de reclamação separadas das de bounce, e pesam reclamações mais fortemente. Uma taxa de reclamação acima de 0,1% é bandeira vermelha. Acima de 0,3% e você está em sérios apuros.
Juntando tudo
Tratamento adequado de bounces é um pipeline: autentique seus e-mails com SPF, DKIM e DMARC, valide endereços antes que entrem na sua lista, processe bounces e reclamações em tempo real via webhooks, mantenha uma lista de supressão e monitore suas taxas.
A Sendkit cuida do trabalho pesado — supressão automática, entrega de webhook, configuração de autenticação. Seu trabalho é conectar o endpoint do webhook, adicionar sua lógica de negócio customizada e ficar de olho nas métricas.
A documentação completa de webhooks está em docs.sendkit.dev/webhooks/introduction. Configure seu endpoint, faça deploy do código de handler deste guia e pare de enviar pra endereços mortos.
Compartilhar este artigo