Webhook guide
How to receive and verify the events Mailronix sends to your endpoint: delivery, signature, content, retries and secret rotation.
How deliveries arrive
Each event is a POST request with a JSON body to the HTTPS address you registered (port 443 or 8443). Answer within 10 seconds: any 2xx status confirms the delivery. Anything else is an error and triggers another attempt, including redirects, which are not followed. Addresses that are not reachable from the public Internet are rejected.
- Mailronix-Signature
- The request signature. See "Verifying the signature".
- Mailronix-Event-Id
- The event identifier, equal to the id field of the body. It is the same on every attempt.
- Mailronix-Delivery-Id
- The delivery identifier. Quote it when you contact support.
- Mailronix-Event-Type
- The event type, equal to the type field of the body.
Verifying the signature
Always verify the signature before trusting the content: anyone who knows your address can send it a request. The signature is the HMAC-SHA256, hex-encoded, of the string "t.body", with the body exactly as you received it.
Mailronix-Signature: t=<unix>,v1=<hmac-sha256>
v1 = hex( HMAC-SHA256( chiave, "<t>." + corpo ) )- Key: the secret the console shows only once looks like "whsec_…". The HMAC key is the bytes that follow the prefix, decoded from unpadded base64url. The secret text itself is not the key.
- Body: compute the HMAC over the raw bytes you received. If your framework parses the JSON before you, get hold of the raw body (in Express, express.raw): rewriting the JSON changes the bytes and the signature will not match.
- Freshness: reject requests where t is more than five minutes away from the current time. t is the time of that single attempt, so a retry hours later still passes the check.
- Several signatures: during the 24 hours after a rotation the header holds two v1 values, one for the new secret and one for the previous one. Accept the request if any of them matches any of the secrets you know.
- Comparison: use a constant-time comparison, as in the examples.
Verification examples
import { createHmac, timingSafeEqual } from 'node:crypto'
// `segreto` è quello mostrato dalla console, «whsec_…». La chiave HMAC sono i
// suoi byte: si toglie il prefisso e si decodifica da base64url.
const chiave = (segreto) => Buffer.from(segreto.replace(/^whsec_/, ''), 'base64url')
// `corpo` è il corpo ESATTO ricevuto (Buffer), non il JSON riscritto.
export function verificaFirma(intestazione, corpo, segreti, toleranza = 300) {
const parti = intestazione.split(',').map((p) => p.trim().split('='))
const t = parti.find(([k]) => k === 't')?.[1]
const firme = parti.filter(([k]) => k === 'v1').map(([, v]) => v)
if (!t || firme.length === 0) return false
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranza) return false
let valida = false
for (const segreto of segreti) {
const atteso = createHmac('sha256', chiave(segreto)).update(`${t}.`).update(corpo).digest()
for (const firma of firme) {
const ricevuta = Buffer.from(firma, 'hex')
if (ricevuta.length === atteso.length && timingSafeEqual(ricevuta, atteso)) valida = true
}
}
return valida
}
Examples run against the real Mailronix signature. If verification fails, answer 401; once you accept the event, answer 200 right away and do the work afterwards.
Events and content
Every body has the same envelope; the content of data depends on the type.
{
"id": "evt_01j9x4k2m7q8r5t3v6w1y0z2ab",
"type": "email.bounced",
"api_version": "2026-10-22",
"created_at": "2026-10-22T14:03:11Z",
"brand_id": "6f1c7d0e-5a3b-4e8f-9c21-0b7d4a1e3f52",
"data": {
"email_log_id": "b2d4f6a8-1c3e-4a5b-8d7f-9e0a1b2c3d4e",
"recipient": "[email protected]",
"category": "transactional",
"bounce_type": "permanent"
}
}- id: the event identifier. Use it to recognise duplicates.
- type: one of the types listed below, or webhook.test for the test delivery.
- api_version: the format version.
- created_at: when the event happened, in UTC.
- brand_id: the brand the event belongs to (null only in webhook.test).
- email.* events carry email_log_id, recipient and category in data. email.bounced adds bounce_type (permanent, transient or undetermined): a bounce announced as transient and later reclassified as permanent produces a second email.bounced.
- contact.* events carry contact_id and email in data. contact_id can be null in contact.unsubscribed, for example when the unsubscription comes from a revocation: handle that case.
Event types
- email.sent
- webhookGuide.eventTypes.email.sent
- email.delivered
- webhookGuide.eventTypes.email.delivered
- email.bounced
- webhookGuide.eventTypes.email.bounced
- email.complained
- webhookGuide.eventTypes.email.complained
- email.failed
- webhookGuide.eventTypes.email.failed
- email.suppressed
- webhookGuide.eventTypes.email.suppressed
- contact.confirmed
- webhookGuide.eventTypes.contact.confirmed
- contact.unsubscribed
- webhookGuide.eventTypes.contact.unsubscribed
Open events are not available: Mailronix does not track them, by privacy choice. email.clicked events only arrive for brands that turned click tracking on and only for recipients who agreed; they describe a detected click (scanners and previews can generate them) and carry the destination host, not the URL. Withdrawal blocks new detections and does not retroactively make an event already collected unlawful: earlier events already queued may arrive later, within the queue’s 30-day retention period. Deleting the contact deletes events and queued deliveries in accordance with the applicable procedure. The customer receiving the webhook remains responsible for its own copies and for handling data subject rights in relation to them.
Retries, duplicates and order
- A failed delivery is retried up to ten times in almost four days: immediately, then after 10 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, 24 hours and 48 hours.
- Each event arrives at least once, not exactly once: it can arrive twice, for example if you answer after the 10-second limit. Keep the ids you have already processed and ignore duplicates.
- Arrival order is not guaranteed: an email.delivered can arrive after an email.bounced. To order events use created_at, not the time you receive them.
- If an event runs out of attempts and no delivery to that endpoint has succeeded in the last three days, the endpoint is disabled and the owner receives an email. From the Webhooks page you can re-enable it (the password is required) and resend single events. Deliveries are kept for 30 days.
Rotating the secret
From the Webhooks page you can rotate an endpoint's secret (the password is required); the new secret is shown only once. For 24 hours every delivery carries two signatures, one for the new secret and one for the previous one: accept both secrets during that time and update your receiver without losing deliveries. If the old secret has leaked, choose to revoke it immediately: from then on only the new one counts.
Testing your receiver
When you create an endpoint, and with the "Send test" action, Mailronix sends a webhook.test event with the same envelope and signature as real events, with no recipient data. If the test fails, the endpoint is saved but stays disabled.
Limits
Active endpoints per account are 1 on the Free plan, 5 on Starter and 10 on Growth. Endpoints are configured only from the console, with a session and never with an API key.