Webhooks
Subscribe to delivery, bounce, complaint, and other email lifecycle events over HTTP with signed, Standard Webhooks-compatible payloads. API reference.
/webhooksCreate a webhook endpointChoose from 21 event types:
email.sentemail.deliveredemail.delivery_delayedemail.bouncedemail.complainedemail.openedemail.clickedemail.failedemail.scheduledemail.suppressedemail.canceledemail.receiveddomain.reputation.warningdomain.reputation.pauseddomain.health.degradeddomain.health.criticalaccount.createdaccount.suspendedaccount.resumeddomain.dmarc.unknown_sourcedomain.dmarc.ramp_readycurl -X POST https://api.irisend.com/webhooks \
-H "Authorization: Bearer $IRISEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"endpoint": "https://yourdomain.com/webhooks/irisend",
"events": ["email.delivered", "email.bounced", "email.complained"]
}'{
"object": "webhook",
"id": "wh_3xC7qR1vN9sL4yB2wT8hM0z",
"signing_secret": "whsec_MfKzB3n2Rp9vQwL7xC1sH5tY"
}The create response only carries signing_secret — fetch GET /webhooks/:id to read back endpoint, events, and status together with the secret.
/webhooksList webhook endpointsOmits signing_secret.
curl https://api.irisend.com/webhooks \
-H "Authorization: Bearer $IRISEND_API_KEY"{
"object": "list",
"has_more": false,
"data": [
{
"object": "webhook",
"id": "wh_3xC7qR1vN9sL4yB2wT8hM0z",
"endpoint": "https://yourdomain.com/webhooks/irisend",
"events": ["email.delivered", "email.bounced", "email.complained"],
"status": "enabled",
"created_at": "2026-09-25T09:00:12.481Z"
}
]
}/webhooks/:idRetrieve a webhook endpointIncludes signing_secret.
curl https://api.irisend.com/webhooks/wh_3xC7qR1vN9sL4yB2wT8hM0z \
-H "Authorization: Bearer $IRISEND_API_KEY"{
"object": "webhook",
"id": "wh_3xC7qR1vN9sL4yB2wT8hM0z",
"endpoint": "https://yourdomain.com/webhooks/irisend",
"events": ["email.delivered", "email.bounced", "email.complained"],
"status": "enabled",
"created_at": "2026-09-25T09:00:12.481Z",
"signing_secret": "whsec_MfKzB3n2Rp9vQwL7xC1sH5tY"
}/webhooks/:idRemove a webhook endpointcurl -X DELETE https://api.irisend.com/webhooks/wh_3xC7qR1vN9sL4yB2wT8hM0z \
-H "Authorization: Bearer $IRISEND_API_KEY"{ "object": "webhook", "id": "wh_3xC7qR1vN9sL4yB2wT8hM0z", "deleted": true }Payload shape
Every delivery is a JSON envelope of { type, created_at, data }. The data object is not one shape — it depends on the event family, so branch on type before reading data. account.* events are delivered to the parent account's webhooks (sub-account lifecycle), not the sub-account's own.
{
"type": "email.delivered",
"created_at": "2026-09-25T09:01:04.112Z",
"data": {
"email_id": "em_9f2K3mQ1zPr7vT6yB4wL0nC",
"from": "[email protected]",
"to": ["[email protected]"],
"subject": "Your receipt",
"created_at": "2026-09-25T09:00:12.481Z",
"tags": { "category": "receipt" }
}
}| Event | data fields |
|---|---|
email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.opened, email.clicked, email.failed, email.scheduled, email.suppressed, email.canceled | { email_id, from, to, subject, created_at, tags } |
email.received | { email_id, created_at, from, to, bcc, cc, received_for, message_id, subject, attachments: [{ id, filename, content_type, content_disposition, content_id }] } |
domain.reputation.warning, domain.reputation.paused | { domain_id, domain, hard_bounce_rate, complaint_rate, sample_size, window } |
domain.health.degraded, domain.health.critical | { domain_id, domain, score, band, factors: [{ key, score, detail }] } |
account.created, account.suspended, account.resumed | { account_id, name, external_id, status, suspended_reason } |
domain.dmarc.unknown_source | { domain_id, domain, source_ip, messages } |
domain.dmarc.ramp_ready | { domain_id, domain, recommended_policy } |
Verifying signatures
Deliveries are signed the Standard Webhooks way, byte-compatible with Svix — the svix and standardwebhooks verifier libraries work unchanged against webhook-id, webhook-timestamp, and webhook-signature (mirrored as svix-id, svix-timestamp, svix-signature). Signatures are valid for 300 seconds from the timestamp. Verify against the raw request body exactly as received — re-serialized JSON won't match — and look headers up by their lowercase names. The Python and PHP SDKs ship this as irisend.webhooks.verify() / Irisend\Webhooks::verify() — see SDKs & tools.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyIrisendWebhook(secret, headers, body) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signature = headers["webhook-signature"];
if (!id || !timestamp || !signature) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = Buffer.from(
createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest("base64"),
);
// headers may carry several space-separated "v1,<sig>" values during rotation
return signature.split(" ").some((part) => {
const candidate = Buffer.from(part.replace(/^v1,/, ""));
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
});
}