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.
Configure your webhook
Section titled “Configure your webhook”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.
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):
GET /v1/client/keys/:id/webhookResponse:
{ "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):
DELETE /v1/client/keys/:id/webhookWebhookEvent payload
Section titled “WebhookEvent payload”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>"}Available events
Section titled “Available events”| Event | Description |
|---|---|
email.sent | Transactional email sent successfully |
email.failed | Transactional email failed to send |
contact.unsubscribed | A contact unsubscribed |
campaign.email.sent | Campaign email sent to a contact |
campaign.email.failed | Campaign email failed for a contact |
campaign.completed | Campaign finished sending to all recipients |
sequence.subscribed | Contact subscribed to a sequence |
sequence.step.sent | Sequence step sent |
sequence.step.failed | Sequence step failed |
sequence.completed | Sequence completed for a contact |
sequence.cancelled | Sequence cancelled (field reason with cause) |
Fields per event
Section titled “Fields per event”email.sent/email.failed:request_id,to(array),subject,message_id.failedalso includeserror.contact.unsubscribed:email.campaign.email.sent/campaign.email.failed:email,campaign_id.failedalso includeserror.campaign.completed:campaign_id,name,sent_count,failed_count,delivery_rate.sequence.*:email,campaign_id(sequence id).sequence.cancelledalso includesreason(unsubscribed,manual,bounced,complained,suppressed).
Verifying the HMAC-SHA256 signature
Section titled “Verifying the HMAC-SHA256 signature”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 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: # Check that the timestamp is not too old (anti-replay) ts = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) diff = abs((datetime.now(timezone.utc) - ts).total_seconds()) if diff > 300: # more than 5 minutes → reject 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)Retries with backoff
Section titled “Retries with backoff”If your endpoint does not respond with a 2xx, MailerDash retries automatically:
- Initial burst (in-process, max 3 attempts): after the first failure, retries at 1s, 5s, and 25s.
- Queue with progressive backoff (if the burst fails completely): 5 min → 30 min → 2 h → 6 h → 24 h (×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.
Management endpoints
Section titled “Management endpoints”Rotate the secret
Section titled “Rotate the secret”Generates a new webhook_secret and invalidates the previous one. Use this if you believe your secret was compromised.
POST /v1/client/keys/:id/webhook/rotate-secretSend a test event
Section titled “Send a test event”Fires a webhook.test event to the configured URL to verify that your endpoint receives it correctly.
POST /v1/client/keys/:id/webhook/testDelivery history
Section titled “Delivery history”Lists the most recent deliveries with their status, HTTP code, and timestamp.
GET /v1/client/keys/:id/webhook/deliveriesQuery parameters: limit (default 50, max 200), offset.
Delivery detail
Section titled “Delivery detail”Includes the full payload and the response body from your endpoint (up to 2 KB).
GET /v1/client/keys/:id/webhook/deliveries/:dIdManually retry a delivery
Section titled “Manually retry a delivery”Re-fires a specific event using the key’s current URL and secret.
POST /v1/client/keys/:id/webhook/deliveries/:dId/retryAdditional security
Section titled “Additional security”- 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_secretis only shown masked in API responses; it is never exposed in full after creation.
See the full reference at /reference/platform/.