Webhooks
Los webhooks te permiten recibir notificaciones en tiempo real cuando ocurren eventos en tu cuenta: envíos entregados, fallos, desuscripciones, y eventos de campañas y sequences.
Los endpoints de webhook son de tu cuenta, no de una API key: reciben los eventos de todas tus keys. Cuando se dispara un evento, MailerDash hace un POST a cada endpoint suscrito con el payload firmado con HMAC-SHA256.
Puedes tener varios —por ejemplo uno para tu app y otro para tu herramienta de soporte— y elegir qué eventos recibe cada uno.
Crear un endpoint
Sección titulada «Crear un endpoint»curl -X POST https://api.mailerdash.com/v1/client/webhook-endpoints \ -H "Authorization: Bearer $MAILERDASH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://mi-app.com/webhooks/mailerdash", "enabled_events": ["*"], "description": "App principal" }'enabled_events acepta ["*"] (todos) o la lista de los que te interesan — ver la tabla de más abajo. description es para que los distingas en el panel.
La respuesta incluye el secret, y es la única vez que se muestra completo:
{ "id": "whe_9f3a2b1c8d7e", "url": "https://mi-app.com/webhooks/mailerdash", "secret": "whsec_...", "enabled_events": "[\"*\"]", "status": "active", "description": "App principal"}Para listar los que tienes (sin exponer secretos) y ver el detalle de uno:
GET /v1/client/webhook-endpointsGET /v1/client/webhook-endpoints/:idPara cambiar la URL, los eventos o desactivarlo sin borrarlo:
PATCH /v1/client/webhook-endpoints/:id{ "url": "https://mi-app.com/webhooks/v2", "status": "active" }Y para eliminarlo:
DELETE /v1/client/webhook-endpoints/:idPayload EventoWebhook
Sección titulada «Payload EventoWebhook»Cada evento llega como un POST con Content-Type: application/json. El cuerpo tiene siempre los campos event, ts y app, más campos específicos según el tipo de evento.
{ "event": "email.sent", "ts": "2026-06-22T15:30:00.000Z", "app": "mi-app", "request_id": "req_abc123", "to": ["usuario@ejemplo.com"], "subject": "Tu factura de junio", "message_id": "<abc123@mailerdash.com>"}Eventos disponibles
Sección titulada «Eventos disponibles»| Evento | Descripción |
|---|---|
email.sent | Correo transaccional enviado exitosamente |
email.failed | Correo transaccional falló al enviarse |
email.rejected | No enviamos el correo: lo rechazamos antes de salir. El campo reason trae el código (domain_unauthorized, quota_exceeded, spam…) |
email.suppressed | No enviamos el correo porque la dirección está en tu lista de supresión. Quítala de tu base o levántala desde el panel |
email.opened | Un destinatario abrió el correo (primer open, vía pixel de tracking) |
email.clicked | Un destinatario hizo click en un link rastreado (primer click) |
email.bounced | Un correo rebotó (hard o soft bounce) |
email.complained | Un destinatario marcó el correo como spam (FBL) |
contact.unsubscribed | Un contacto se desuscribió |
contact.confirmed | Un contacto confirmó su suscripción (doble opt-in) |
campaign.email.sent | Email de campaña enviado a un contacto |
campaign.email.failed | Email de campaña falló para un contacto |
campaign.completed | Campaña completó el envío a todos sus destinatarios |
sequence.subscribed | Contacto suscrito a una sequence |
sequence.step.sent | Paso de sequence enviado |
sequence.step.failed | Paso de sequence falló |
sequence.completed | Sequence completada para un contacto |
sequence.cancelled | Sequence cancelada (campo reason con motivo) |
dmarc.alignment_failure | Tu dominio superó el umbral de fallos de alineación DMARC |
Campos por evento
Sección titulada «Campos por evento»email.sent/email.failed:request_id,to(array),subject,message_id. Enfailedincluyeerror.email.opened:campaign_id,email.email.clicked:campaign_id,email,url.email.bounced:recipient,status,category,diagnostic,message_id.email.complained:email,isp,subject.contact.unsubscribed:email.contact.confirmed:email,list_id,list_name.campaign.email.sent/campaign.email.failed:email,campaign_id. Enfailedincluyeerror.campaign.completed:campaign_id,name,sent_count,failed_count,delivery_rate.sequence.*:email,campaign_id(id de la sequence). Ensequence.cancelledincluyereason(unsubscribed,manual,bounced,complained,suppressed).dmarc.alignment_failure:domain,fail_rate,volume,policy_p,window_days,top_failing_sources.
Verificar la firma HMAC-SHA256
Sección titulada «Verificar la firma HMAC-SHA256»Cada request incluye dos headers de seguridad:
X-Md-Signature:sha256=<hmac-hex>X-Md-Timestamp: timestamp ISO en el momento del envío
La firma se calcula sobre el string <timestamp>.<cuerpo_crudo> usando tu webhook_secret.
const crypto = require('crypto');
function verifyWebhook(req, webhookSecret) { const signature = req.headers['x-md-signature']; // "sha256=abc123..." const timestamp = req.headers['x-md-timestamp']; // "2026-06-22T15:30:00.000Z" const rawBody = req.body; // Buffer o string — debe ser el cuerpo crudo sin parsear
if (!signature || !timestamp) return false;
// Verificar que el timestamp no sea demasiado viejo (anti-replay) const diff = Math.abs(Date.now() - new Date(timestamp).getTime()); if (diff > 5 * 60 * 1000) return false; // más de 5 minutos → rechazar
// Calcular HMAC sobre "<timestamp>.<cuerpo_crudo>" const signedPayload = `${timestamp}.${rawBody}`; const expected = 'sha256=' + crypto .createHmac('sha256', webhookSecret) .update(signedPayload) .digest('hex');
// Comparar con tiempo constante para evitar timing attacks return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) );}
// Ejemplo con Express — parsea el body como Buffer para preservar el raw bodyapp.post('/webhooks/mailerdash', express.raw({ type: 'application/json' }), (req, res) => { const valid = verifyWebhook(req, process.env.MAILERDASH_WEBHOOK_SECRET); if (!valid) return res.status(401).send('Invalid signature');
const event = JSON.parse(req.body); console.log('Evento recibido:', event.event); res.sendStatus(200);});import hmacimport hashlibimport timefrom datetime import datetime, timezone
def verify_webhook(signature: str, timestamp: str, raw_body: bytes, secret: str) -> bool: # Verificar que el timestamp no sea demasiado viejo (anti-replay) ts = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) diff = abs((datetime.now(timezone.utc) - ts).total_seconds()) if diff > 300: # más de 5 minutos → rechazar return False
signed_payload = f"{timestamp}.{raw_body.decode('utf-8')}".encode('utf-8') expected = 'sha256=' + hmac.new( secret.encode('utf-8'), signed_payload, hashlib.sha256 ).hexdigest()
return hmac.compare_digest(signature, expected)Reintentos con backoff
Sección titulada «Reintentos con backoff»Si tu endpoint no responde con un 2xx, MailerDash reintenta automáticamente:
- Burst inicial (in-process, máx. 3 intentos): después del primer fallo, reintenta a los 1s, 5s y 25s.
- Queue con backoff progresivo (si el burst falla por completo): 5 min → 30 min → 2 h → 6 h → 24 h (×3).
- Si agotan todos los intentos, el delivery queda en estado
failed(DLQ) visible desde el historial.
El timeout por intento es de 10 segundos. Tu endpoint debe responder dentro de ese tiempo.
Endpoints de gestión
Sección titulada «Endpoints de gestión»Rotar el secreto
Sección titulada «Rotar el secreto»Genera un secreto nuevo e invalida el anterior al instante. Úsalo si crees que el tuyo se filtró — y ten listo el cambio en tu servidor, porque entre la rotación y el despliegue tus entregas van a fallar la verificación de firma.
POST /v1/client/webhook-endpoints/:id/rotate-secretTambién te avisamos por correo cuando pasa, por la misma razón que el cambio de URL.
Enviar un evento de prueba
Sección titulada «Enviar un evento de prueba»Dispara un webhook.test a la URL del endpoint para verificar que tu servidor lo recibe y valida la firma.
POST /v1/client/webhook-endpoints/:id/testHistorial de entregas
Sección titulada «Historial de entregas»Lista las últimas entregas con su estado, código HTTP y timestamp.
GET /v1/client/webhook-endpoints/:id/deliveriesParámetros de query: limit (default 50, máx. 200), offset.
Detalle de una entrega
Sección titulada «Detalle de una entrega»Incluye el payload completo y el body de respuesta de tu endpoint (hasta 2 KB).
GET /v1/client/webhook-endpoints/:id/deliveries/:dIdReintentar una entrega manualmente
Sección titulada «Reintentar una entrega manualmente»Re-dispara un evento específico con la URL y el secreto actuales del endpoint.
POST /v1/client/webhook-endpoints/:id/deliveries/:dId/retrySeguridad adicional
Sección titulada «Seguridad adicional»- En producción, MailerDash solo entrega a URLs HTTPS.
- Las URLs que resuelven a IPs privadas o reservadas (loopback, RFC1918, metadata de cloud) son bloqueadas para prevenir SSRF.
- El secreto solo se muestra completo al crear el endpoint y al rotarlo; en el resto de las respuestas va enmascarado.
Ver referencia completa en /reference/platform/.