Paulo CastellanoCómo manejar bounces de email programáticamente
Construye un manejo adecuado de bounces con webhooks, listas de supresión y lógica de reintentos para proteger tu reputación de remitente.

Cada email que envías a una dirección muerta es una señal para los proveedores de email de que no sabes lo que estás haciendo. Suficientes de esas señales y tus emails dejan de llegar a personas reales. El manejo de bounces no es un nice-to-have. Es infraestructura que protege tu capacidad de enviar email en absoluto.
Esta guía recorre la construcción de un manejo programático de bounces con el email API de Sendkit: endpoints webhook, listas de supresión, lógica de reintentos y procesamiento de quejas. Al final tendrás un sistema que mantiene tu tasa de bounces muy por debajo de los umbrales que importan.
Qué son los bounces de email
Un bounce ocurre cuando un email no puede ser entregado al destinatario. El servidor de correo receptor devuelve un rechazo, y tu proveedor de email lo captura. Hay dos tipos.
Los hard bounces son fallos permanentes. La dirección de email no existe, el dominio es inválido o el servidor del destinatario ha bloqueado explícitamente la entrega. Códigos SMTP comunes: 550 (mailbox not found), 551 (user not local), 552 (mailbox full, a veces clasificado como hard). Una vez que recibes un hard bounce, esa dirección está muerta para ti. No le envíes de nuevo.
Los soft bounces son fallos temporales. El buzón está lleno, el servidor está temporalmente caído, se alcanzó un rate limit o hay un problema DNS en el lado receptor. Códigos SMTP en el rango 4xx. Estos pueden resolverse por sí solos, por lo que reintentar tiene sentido, pero solo hasta cierto punto.
La distinción importa porque tu lógica de manejo debe ser completamente diferente para cada tipo.
Por qué los bounces dañan la reputación del remitente
Los proveedores de email rastrean tu tasa de bounces como una señal central para la reputación del remitente. Google y Yahoo aplicaron un techo estricto en 2024: si tu tasa de bounces supera el 2%, empiezas a ser throttled o bloqueado. Microsoft siguió con aplicación similar en Outlook.com y Hotmail en 2025.
Las cuentas son simples. Si envías 10.000 emails y 300 rebotan, estás en 3%. Eso es suficiente para que Gmail empiece a enrutar tu correo a spam para todos en tu lista, incluyendo las direcciones válidas. Tu entregabilidad se desploma, las tasas de apertura caen y estás atascado en un hoyo que toma semanas salir.
La solución no es complicada: deja de enviar a direcciones que rebotan y valida las direcciones antes de que entren a tu lista. Pero necesitas la plomería para que eso ocurra automáticamente.
Para una mirada más profunda a todos los factores que afectan la colocación en bandeja de entrada, lee nuestra guía para mejorar la entregabilidad del email.
Configurando un endpoint webhook para eventos de bounce
Sendkit envía eventos webhook en tiempo real cuando algo pasa con un email que enviaste. Los eventos relevantes para el manejo de bounces son:
email.bounced— el email rebotó (hard o soft)email.complained— el destinatario marcó tu email como spamemail.delivered— confirmación de entrega exitosaemail.failed— el email falló al enviar por completo
Configuras endpoints webhook en el dashboard de Sendkit o vía la API. Sendkit firma cada payload webhook con HMAC-SHA256, enviado en el header x-sendkit-signature, para que puedas verificar que la solicitud es legítima.

Construyendo el manejador del webhook
Aquí hay un manejador webhook completo de Express.js que verifica la firma y enruta eventos a la lógica de procesamiento correcta.
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 });
});Siempre verifica la firma antes de procesar cualquier payload webhook. Sin verificación, cualquiera puede enviar eventos de bounce falsos a tu endpoint y corromper tu lista de supresión.
Procesando hard bounces
Los hard bounces no son negociables. La dirección está muerta. Supriméla inmediatamente y nunca le envíes de nuevo.
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);
}
};Sin reintento. Sin segundas oportunidades. Un hard bounce significa que el buzón no existe. Reintentar solo aumenta tu tasa de bounces y daña tu reputación aún más.
Procesando soft bounces con backoff exponencial
Los soft bounces merecen reintentos, pero con límites. Reintenta algunas veces con retrasos crecientes. Si la dirección sigue fallando, trátala como un 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);
};Tres strikes es un umbral razonable. Después de tres soft bounces, la dirección está permanentemente rota o es consistentemente poco fiable. De cualquier manera, no la quieres en tu lista.

Construyendo una lista de supresión
Tu lista de supresión es una tabla de base de datos que revisas antes de cada envío individual. Sin excepciones. El esquema es sencillo:
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 envío, verifica la 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,
});
};Para envíos masivos, consulta la lista de supresión en batch en lugar de una a la vez. Extrae todas las direcciones suprimidas para tu lista de destinatarios en una sola consulta y fíltralas antes de empezar a enviar.
Si estás gestionando contactos a través de Sendkit, puedes sincronizar el estado de supresión para mantener todo consistente en tu infraestructura de envío.
Supresión automática de Sendkit
Aquí está la buena noticia: Sendkit mantiene su propia lista de supresión automáticamente. Cuando vuelve un hard bounce, Sendkit suprime esa dirección en toda tu cuenta. No necesitas construir nada de lo anterior para obtener protección básica.
Entonces, ¿por qué construir la tuya propia? Tres razones.
Lógica personalizada. Podrías querer umbrales diferentes para diferentes tipos de email. Los emails transaccionales (resets de contraseña, confirmaciones de pedido) podrían justificar reintentos más agresivos que los emails de marketing. Tu propia capa de supresión te permite tomar esas decisiones.
Sincronización entre sistemas. Si envías desde múltiples servicios o tienes direcciones en un CRM, tus datos de supresión necesitan propagarse a todos lados. Tu propia base de datos es la fuente de verdad.
Pista de auditoría. Cuando alguien pregunte por qué dejó de recibir emails, puedes mostrar exactamente cuándo ocurrió la supresión, qué la causó y qué evento de bounce la disparó.
Piensa en la supresión automática de Sendkit como la red de seguridad y tu lógica personalizada como la capa afinada encima.
Monitoreando tasas de bounces
Necesitas visibilidad de tu tasa de bounces en cada envío. Configura una función de monitoreo simple:
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 };
};Configura tu umbral de alerta en 1,5%, no en 2%. Quieres detectar problemas antes de que activen la aplicación de Google, Yahoo o Microsoft. Para cuando estés en 2%, el daño ya está hecho.
La mejor forma de mantener bajas las tasas de bounces es evitar que las direcciones malas entren a tu lista en primer lugar. Ejecuta validación de email en el punto de captura y periódicamente contra tu lista existente. Explicamos cómo en nuestra guía para validar direcciones de email antes de enviar.
Manejando quejas
Los eventos de queja (email.complained) se disparan cuando un destinatario hace clic en "Reportar como spam" en su cliente de email. Esto es peor que un bounce. Un bounce significa que la dirección es mala. Una queja significa que una persona real activamente no quiere tu email.
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' },
});
};Suprime inmediatamente. No envíes un email de "lamento verte ir". No los agregues a un flujo de reactivación. Te reportaron como spam. La única respuesta correcta es dejar de enviarles.
Las postmaster tools de Google muestran las tasas de quejas por separado de las tasas de bounces, y pesan más las quejas. Una tasa de quejas por encima del 0,1% es una bandera roja. Por encima del 0,3% y estás en serios problemas.
Juntándolo todo
El manejo adecuado de bounces es un pipeline: autentica tus emails con SPF, DKIM y DMARC, valida direcciones antes de que entren a tu lista, procesa bounces y quejas en tiempo real vía webhooks, mantén una lista de supresión y monitorea tus tasas.
Sendkit maneja el trabajo pesado — supresión automática, entrega de webhooks, configuración de autenticación. Tu trabajo es conectar el endpoint webhook, agregar tu lógica de negocio personalizada y estar atento a las métricas.
La documentación completa de webhooks está en docs.sendkit.dev/webhooks/introduction. Configura tu endpoint, despliega el código del manejador de esta guía y deja de enviar a direcciones muertas.
Compartir este artículo