Skip to content

Webhooks

Webhooks let you receive real-time notifications when events occur in your account: delivered emails, failures, unsubscribes, and campaign and sequence events.

Each API key can have its own webhook_url. When an event fires, MailerDash sends a POST to that URL with the event payload signed with HMAC-SHA256.


The webhook is configured per API key using PATCH /v1/client/keys/:id with the fields webhook_url (and optionally a webhook_secret you can rotate later). If no secret exists when you set the URL, the system generates one automatically.

Ventana de terminal
curl -X PATCH https://api.mailerdash.com/v1/client/keys/mi-key \
-H "Authorization: Bearer $MAILERDASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "webhook_url": "https://mi-app.com/webhooks/mailerdash" }'

To view the current webhook configuration (without exposing the secret):

Ventana de terminal
GET /v1/client/keys/:id/webhook

Response:

{
"webhook_url": "https://mi-app.com/webhooks/mailerdash",
"secret_set": true,
"secret_masked": "whsec_••••••••••••3a9f"
}

To disable the webhook (URL + secret are set to null):

Ventana de terminal
DELETE /v1/client/keys/:id/webhook

Each event arrives as a POST with Content-Type: application/json. The body always contains the fields event, ts, and app, plus additional fields specific to the event type.

{
"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>"
}
EventDescription
email.sentTransactional email sent successfully
email.failedTransactional email failed to send
contact.unsubscribedA contact unsubscribed
campaign.email.sentCampaign email sent to a contact
campaign.email.failedCampaign email failed for a contact
campaign.completedCampaign finished sending to all recipients
sequence.subscribedContact subscribed to a sequence
sequence.step.sentSequence step sent
sequence.step.failedSequence step failed
sequence.completedSequence completed for a contact
sequence.cancelledSequence cancelled (field reason with cause)
  • email.sent / email.failed: request_id, to (array), subject, message_id. failed also includes error.
  • contact.unsubscribed: email.
  • campaign.email.sent / campaign.email.failed: email, campaign_id. failed also includes error.
  • campaign.completed: campaign_id, name, sent_count, failed_count, delivery_rate.
  • sequence.*: email, campaign_id (sequence id). sequence.cancelled also includes reason (unsubscribed, manual, bounced, complained, suppressed).

Each request includes two security headers:

  • X-Md-Signature: sha256=<hmac-hex>
  • X-Md-Timestamp: ISO timestamp at the time of delivery

The signature is computed over the string <timestamp>.<raw_body> using your 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 or string — must be the raw unparsed body
if (!signature || !timestamp) return false;
// Check that the timestamp is not too old (anti-replay)
const diff = Math.abs(Date.now() - new Date(timestamp).getTime());
if (diff > 5 * 60 * 1000) return false; // more than 5 minutes → reject
// Compute HMAC over "<timestamp>.<raw_body>"
const signedPayload = `${timestamp}.${rawBody}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', webhookSecret)
.update(signedPayload)
.digest('hex');
// Compare in constant time to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Example with Express — parse body as Buffer to preserve the 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);
});

If your endpoint does not respond with a 2xx, MailerDash retries automatically:

  1. Initial burst (in-process, max 3 attempts): after the first failure, retries at 1s, 5s, and 25s.
  2. Queue with progressive backoff (if the burst fails completely): 5 min → 30 min → 2 h → 6 h → 24 h (×3).
  3. If all attempts are exhausted, the delivery is marked as failed (DLQ) and visible in the delivery history.

The timeout per attempt is 10 seconds. Your endpoint must respond within that time.


Generates a new webhook_secret and invalidates the previous one. Use this if you believe your secret was compromised.

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

Fires a webhook.test event to the configured URL to verify that your endpoint receives it correctly.

Ventana de terminal
POST /v1/client/keys/:id/webhook/test

Lists the most recent deliveries with their status, HTTP code, and timestamp.

Ventana de terminal
GET /v1/client/keys/:id/webhook/deliveries

Query parameters: limit (default 50, max 200), offset.

Includes the full payload and the response body from your endpoint (up to 2 KB).

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

Re-fires a specific event using the key’s current URL and secret.

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

  • In production, MailerDash only delivers to HTTPS URLs.
  • URLs that resolve to private or reserved IPs (loopback, RFC1918, cloud metadata) are blocked to prevent SSRF.
  • The webhook_secret is only shown masked in API responses; it is never exposed in full after creation.

See the full reference at /reference/platform/.