Guide to Iterable Webhooks: Features and Best Practices
Iterable webhooks stream what happens to your messages and users. Every email sent, opened, clicked, bounced or marked as spam, every push, SMS, in-app or WhatsApp message delivered, and every subscribe or unsubscribe can be sent to your own endpoint as it happens. Teams use them to feed a data warehouse, trigger downstream workflows, keep suppression lists in sync and build their own engagement reporting.
This guide covers how system webhooks are set up and authenticated, the events they can carry and the shape of their payloads, the tight timeout and narrow retry rules that decide whether an event survives, and the practices that make an Iterable receiver dependable.
What are Iterable webhooks?
Iterable has two kinds of webhook. A journey webhook is a tile in a journey that calls a URL with a body you compose. A system webhook, the subject of this guide, is project-wide: you pick a set of triggering events, and Iterable sends a POST every time one of them happens anywhere in the project, whichever campaign, template or journey caused it.
Each request is a JSON body with the user's email and/or userId, an eventName, and a dataFields object with the event's details.
Iterable does not sign system webhooks. Instead it offers an Authentication setting that puts a static value in the Authorization header: Basic, which sends exactly the token you enter, or OAuth 2.0, which sends Bearer <token>. Comparing that header with the value you configured is how you tell a real Iterable request from a forged one.
Iterable webhook features
| Feature | Details |
|---|---|
| Configuration | Integrations > System Webhooks (Manage Integrations permission or org admin) |
| Scope | One project. Not tied to a template, campaign or journey |
| Initial status | Disabled until you set Status to Enabled |
| Method and format | POST, Content-Type: application/json |
| Endpoint | HTTPS with a certificate signed by a CA Iterable trusts |
| Authentication | None, Basic (your token verbatim in Authorization) or OAuth 2.0 (Authorization: Bearer <token>) |
| Custom headers | Add HTTP Header, name and value. Visible to anyone with Manage Integrations |
| Signature and timestamp | None |
| Event field | eventName in the body |
| Timeout | 2 seconds |
| Retries | Up to 3, with increasing delays over a few minutes, only for 429, 502, 503, 504 and timeouts |
| Not retried | Other 4xx and 5xx, DNS errors, connection errors and TLS failures |
| Guarantees | Delivery and order are not guaranteed, and duplicates are possible |
| Circuit breaker | At least 500 errors and a 90% error rate in 10 seconds pauses delivery for 60 seconds |
| Merge tags | In the URL and custom headers, for example {{campaignId}} |
| Source IPs | Not published |
Common events
| Channel | Events |
|---|---|
emailSend (blast and triggered), emailClick, emailOpen, emailSubscribe, emailUnSubscribe, hostedUnsubscribeClick, emailBounce, emailComplaint, emailSendSkip | |
| Embedded | embeddedClick, embeddedImpression, embeddedReceived, embeddedSession |
| In-app | inAppClick, inAppClose, inAppDelete, inAppDelivery, inAppOpen, inAppRecall, inAppSend, inAppSendSkip |
| Push | pushSend, pushOpen, pushBounce, pushUninstall, pushSendSkip |
| RCS | rcsSend, rcsSendSkip, rcsBounce, rcsSeen, rcsClick |
| SMS | smsSend, smsClick, smsReceived, smsBounce, smsSendSkip |
| Web push | webPushSend, webPushClick, webPushSendSkip |
whatsAppSend, whatsAppReceived, whatsAppBounce, whatsAppSeen, whatsAppClick, whatsAppSendSkip | |
| Other | anonSession, unknownSession, journeyExit |
emailSubscribe and emailUnSubscribe are universal: they cover subscription changes on every channel, not just email. Iterable spells the unsubscribe event emailUnSubscribe in its event list and emailUnsubscribe elsewhere in the same article, so match it case-insensitively until you have seen one on the wire. Blast and triggered email sends share emailSend; dataFields.campaignId and dataFields.workflowId tell them apart.
Here is Iterable's blast emailSend example, trimmed:
{
"email": "user@example.com",
"eventName": "emailSend",
"dataFields": {
"contentId": 331201,
"email": "user@example.com",
"createdAt": "2016-12-02 20:21:04 +00:00",
"campaignId": 59667,
"templateId": 93849,
"messageId": "9c438a20d26e4e9f8ad4edea23f811cd",
"emailSubject": "My subject",
"campaignName": "My campaign name",
"workflowId": null,
"channelId": 3420,
"messageTypeId": 3866,
"emailId": "c123456:t456789:user@example.com"
}
}
Whether email, userId or both appear depends on your project type and the user's profile. Iterable is explicit that its examples are not schemas: it can add fields at any time, and the fields vary with the event, your webhook settings and your project, so test with real data.
Inspect Iterable webhooks as they arrive. Point a system webhook at a Hookdeck Inspect URL to see the exact headers and
dataFieldsyour project sends, with no account or setup required.
Setting up Iterable webhooks
Generate a long random token first, for example with openssl rand -hex 32.
- Go to Integrations > System Webhooks and click Create Webhook.
- Enter your
https://URL and click Create. Iterable opens the Edit System Webhook screen. - Under Authentication, choose OAuth 2.0 and enter only the token, without
Bearer. Iterable sendsAuthorization: Bearer <token>. Or choose Basic and enter the full value you want sent, such asBasic <token>: Iterable adds no prefix and does no encoding. - Under triggering events, select only the events you need. Turn on Exclude List Update Events unless you want subscribe and unsubscribe events from list uploads and list API calls.
- Set Status to Enabled and click Save Webhook.
The token is hidden once saved, so store it somewhere safe before you save. Changes to an existing webhook take a little while to propagate, and Iterable may keep sending to an old URL for a short time after you change it.
To route through Hookdeck, create a Hookdeck source with the Iterable type and use its URL in step 2. The source has one field, Authorization Header, and it needs the full value Iterable sends, prefix included: Bearer <token> for OAuth 2.0, or exactly what you typed for Basic. The source compares the whole value, so it checks the Authentication setting, not a custom header. It accepts POST.
Securing Iterable webhooks
Compare the whole Authorization header with the value Iterable sends, in constant time. Do not strip the Bearer or Basic prefix and do not decode the value.
const crypto = require('crypto');
const express = require('express');
// "Bearer <token>" for OAuth 2.0, or exactly what you typed for Basic.
const EXPECTED = process.env.ITERABLE_AUTHORIZATION;
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
app.post('/webhooks/iterable', express.raw({ type: '*/*' }), (req, res) => {
const received = req.get('authorization');
if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
return res.sendStatus(401);
}
let event;
try {
event = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// Put the event on a queue and respond: Iterable times out after 2 seconds.
// A worker then routes on event.eventName.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
EXPECTED = os.environ.get("ITERABLE_AUTHORIZATION", "") # e.g. "Bearer 3f9c..."
def verify_iterable_request(headers) -> bool:
if not EXPECTED:
return False # never accept when unconfigured
received = headers.get("authorization")
if not received:
return False
# Compare bytes: compare_digest raises TypeError on a non-ASCII str.
return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))
A static Authorization value authenticates the sender, not the body. It cannot detect a modified payload, and with no timestamp a captured request can be replayed and still pass. Use HTTPS, which Iterable requires anyway, a long random token, and rotate it if it might have leaked. Prefer the Authentication setting over a custom header for the secret: Iterable hides the token after saving, while custom headers stay visible to anyone with the Manage Integrations permission.
Make Iterable webhooks production-ready. Hookdeck Event Gateway checks the
Authorizationvalue you configure, answers well inside Iterable's 2-second timeout, and queues every engagement event durably for your service.
Iterable webhook limitations and pain points
Two seconds to respond
The Problem: A handler that writes to a database or calls another API before responding times out, and Iterable retries an event you may already have processed.
Why It Happens: Iterable gives each request 2 seconds. A slower response counts as a failure even if your endpoint finishes the work afterwards, and the retry arrives as a duplicate.
Workarounds:
- Do nothing in the request path except check the header, enqueue the event and return
200. - Deduplicate, because a timeout followed by a retry is the normal way duplicates happen.
How Hookdeck Can Help: Hookdeck accepts the request and responds to Iterable straight away, then delivers to your service with a delivery timeout you choose, so slow processing no longer causes Iterable retries.
Most failures are never retried
The Problem: A 500 from a bug, a 401 from a rotated token, or a DNS or TLS problem loses the event outright.
Why It Happens: Iterable retries only 429, 502, 503, 504 and timeouts, at most three times over a few minutes. Every other error drops the event, and Iterable says plainly that system webhooks are not a definitive data export.
Workarounds:
- Return
503rather than500for transient failures in your own code. - Use Iterable's export APIs or Data Sync for anything that must be complete, and treat webhooks as the fast path.
How Hookdeck Can Help: Hookdeck stores every request it accepts and delivers it to your service with retries on your schedule, whatever your service returns, and you can replay any event after a fix.
The circuit breaker pauses everything
The Problem: A burst of errors from your endpoint pauses all delivery for a minute, including events unrelated to the failure.
Why It Happens: When an endpoint returns at least 500 errors with an error rate of 90% or more within 10 seconds, Iterable pauses delivery to it for 60 seconds.
Workarounds:
- Keep the webhook endpoint thin and separate from the code that does the work.
- Monitor your endpoint's error rate and alert on it, as Iterable recommends.
How Hookdeck Can Help: Hookdeck's ingestion stays up when your service does not, so Iterable never sees your error rate. Issues alert you to failing deliveries, and a max delivery rate protects your service during bursts.
Basic is not HTTP Basic
The Problem: Every request fails your check, or passes a check you thought was stricter, because the header is not what you expected.
Why It Happens: With Basic, Iterable sends exactly the token you type, with no Basic prefix unless you type it and no base64 encoding unless you encode it yourself. With OAuth 2.0, it adds Bearer . The token is hidden after saving, so you cannot look it up later.
Workarounds:
- Decide on the full header value before you save, store it, and compare against it whole.
- If you lose the token, set a new one rather than guessing.
How Hookdeck Can Help: Hookdeck's Iterable source compares the full Authorization value you paste. Hookdeck answers with a 200 either way, but a request with a missing or wrong value is recorded as failed verification and never reaches your service, and you can see exactly what Iterable sent.
List updates flood the endpoint
The Problem: A CSV upload or a bulk list API call produces a burst of emailSubscribe and emailUnSubscribe events that has nothing to do with user behaviour.
Why It Happens: Adding users to or removing them from lists records subscribe and unsubscribe events in their history, and those trigger system webhooks by default.
Workarounds:
- Turn on Exclude List Update Events so only user-initiated subscription changes are sent.
- Select only the triggering events you use.
How Hookdeck Can Help: Hookdeck absorbs the burst and delivers at a rate your service can handle, and filters can drop event types you do not need before they reach you.
Best practices
Store the full Authorization value
Keep Bearer <token>, or your exact Basic value, as one string and compare it whole. Use the Authentication setting for the secret rather than a custom header.
Deduplicate on the event, not the request
Iterable advises using the event timestamp to deduplicate. A key such as eventName plus dataFields.messageId (where present) plus dataFields.createdAt and the user identifier works across retries. See our guide to webhook idempotency.
Do not depend on order
Events can arrive out of order, so an emailOpen may arrive before its emailSend. Store events and derive state from their timestamps rather than from arrival order.
Treat payloads as open-ended
Iterable can add fields at any time and its examples are not schemas. Read the fields you need, ignore the rest, and do not reject unknown fields.
Acknowledge fast and process asynchronously
With a 2-second timeout, asynchronous processing is not optional. See why to process webhooks asynchronously.
Conclusion
Iterable system webhooks send every selected engagement event in a project as an unsigned JSON POST, authenticated only by a static Authorization value from the Basic or OAuth 2.0 setting. Compare that full value in constant time, respond inside 2 seconds, deduplicate on the event, and remember that most failures are dropped after at most three retries.
Hookdeck Event Gateway checks the header, responds to Iterable immediately, and keeps every event until your service has processed it, so a slow handler or a short outage stops costing you engagement data.
Get started with Hookdeck for free and handle Iterable webhooks reliably in minutes.