Docs

Webhooks

Subscribe to delivery, bounce, complaint, and other email lifecycle events over HTTP with signed, Standard Webhooks-compatible payloads. API reference.

POST/webhooksCreate a webhook endpoint

Choose 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_ready
curl -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"]
  }'
ResponseJSON
{
  "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.

GET/webhooksList webhook endpoints

Omits signing_secret.

curl https://api.irisend.com/webhooks \
  -H "Authorization: Bearer $IRISEND_API_KEY"
ResponseJSON
{
  "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"
    }
  ]
}
GET/webhooks/:idRetrieve a webhook endpoint

Includes signing_secret.

curl https://api.irisend.com/webhooks/wh_3xC7qR1vN9sL4yB2wT8hM0z \
  -H "Authorization: Bearer $IRISEND_API_KEY"
ResponseJSON
{
  "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"
}
DELETE/webhooks/:idRemove a webhook endpoint
curl -X DELETE https://api.irisend.com/webhooks/wh_3xC7qR1vN9sL4yB2wT8hM0z \
  -H "Authorization: Bearer $IRISEND_API_KEY"
ResponseJSON
{ "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.

email.deliveredJSON
{
  "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" }
  }
}
Eventdata 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);
  });
}