Ir al contenido

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.


Ventana de terminal
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:

Ventana de terminal
GET /v1/client/webhook-endpoints
GET /v1/client/webhook-endpoints/:id

Para cambiar la URL, los eventos o desactivarlo sin borrarlo:

Ventana de terminal
PATCH /v1/client/webhook-endpoints/:id
{ "url": "https://mi-app.com/webhooks/v2", "status": "active" }

Y para eliminarlo:

Ventana de terminal
DELETE /v1/client/webhook-endpoints/:id

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>"
}
EventoDescripción
email.sentCorreo transaccional enviado exitosamente
email.failedCorreo transaccional falló al enviarse
email.rejectedNo enviamos el correo: lo rechazamos antes de salir. El campo reason trae el código (domain_unauthorized, quota_exceeded, spam…)
email.suppressedNo 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.openedUn destinatario abrió el correo (primer open, vía pixel de tracking)
email.clickedUn destinatario hizo click en un link rastreado (primer click)
email.bouncedUn correo rebotó (hard o soft bounce)
email.complainedUn destinatario marcó el correo como spam (FBL)
contact.unsubscribedUn contacto se desuscribió
contact.confirmedUn contacto confirmó su suscripción (doble opt-in)
campaign.email.sentEmail de campaña enviado a un contacto
campaign.email.failedEmail de campaña falló para un contacto
campaign.completedCampaña completó el envío a todos sus destinatarios
sequence.subscribedContacto suscrito a una sequence
sequence.step.sentPaso de sequence enviado
sequence.step.failedPaso de sequence falló
sequence.completedSequence completada para un contacto
sequence.cancelledSequence cancelada (campo reason con motivo)
dmarc.alignment_failureTu dominio superó el umbral de fallos de alineación DMARC
  • email.sent / email.failed: request_id, to (array), subject, message_id. En failed incluye error.
  • 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. En failed incluye error.
  • campaign.completed: campaign_id, name, sent_count, failed_count, delivery_rate.
  • sequence.*: email, campaign_id (id de la sequence). En sequence.cancelled incluye reason (unsubscribed, manual, bounced, complained, suppressed).
  • dmarc.alignment_failure: domain, fail_rate, volume, policy_p, window_days, top_failing_sources.

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 body
app.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);
});

Si tu endpoint no responde con un 2xx, MailerDash reintenta automáticamente:

  1. Burst inicial (in-process, máx. 3 intentos): después del primer fallo, reintenta a los 1s, 5s y 25s.
  2. Queue con backoff progresivo (si el burst falla por completo): 5 min → 30 min → 2 h → 6 h → 24 h (×3).
  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.


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.

Ventana de terminal
POST /v1/client/webhook-endpoints/:id/rotate-secret

También te avisamos por correo cuando pasa, por la misma razón que el cambio de URL.

Dispara un webhook.test a la URL del endpoint para verificar que tu servidor lo recibe y valida la firma.

Ventana de terminal
POST /v1/client/webhook-endpoints/:id/test

Lista las últimas entregas con su estado, código HTTP y timestamp.

Ventana de terminal
GET /v1/client/webhook-endpoints/:id/deliveries

Parámetros de query: limit (default 50, máx. 200), offset.

Incluye el payload completo y el body de respuesta de tu endpoint (hasta 2 KB).

Ventana de terminal
GET /v1/client/webhook-endpoints/:id/deliveries/:dId

Re-dispara un evento específico con la URL y el secreto actuales del endpoint.

Ventana de terminal
POST /v1/client/webhook-endpoints/:id/deliveries/:dId/retry

  • 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/.