All articlesTransactional & SMTP

    Email Webhooks: Tracking Opens, Bounces, and Clicks

    Email webhooks push delivery, bounce, click, and complaint events to your application in real time instead of making you poll an API. This guide covers the event types and how far to trust each one, payload structure, building an endpoint that acknowledges fast and processes async, signature verification, handling duplicates and out-of-order delivery, and automatic suppression.

    Email Webhooks: Tracking Opens, Bounces, and Clicks
    Erin Moore
    Erin Moore
    September 18, 20269 min read
    Share:

    An email webhook is an HTTP POST your sending platform makes to a URL you control every time something happens to a message — delivered, opened, clicked, bounced, complained. Instead of polling an API for status, you receive events as they occur, which is the only practical way to keep application state in sync with what the mail system actually did.

    Why webhooks instead of polling

    Polling works fine when you send a hundred messages a day. You ask the API for message status on a schedule, reconcile, move on. It falls apart as soon as volume grows, because you end up making thousands of requests to discover that nothing changed.

    Webhooks invert the flow. The platform tells you. Latency drops from your polling interval to roughly the time it takes to make one HTTP request, and your request volume drops to zero. For transactional mail — password resets, receipts, alerts — that latency difference is the whole point. You want to know within seconds that a password reset hard-bounced, not on the next hourly sweep.

    The tradeoff is that you now own an endpoint that has to be available, fast, and correct. That is what the rest of this guide is about.

    Event types you will receive

    EventMeaningReliabilityWhat your app should do
    processed / acceptedPlatform accepted the message for sendingHighRecord the message ID; nothing else
    deliveredReceiving server accepted the messageHighMark as sent; stop retry logic
    bounce (hard)Permanent failure — bad address, domain goneHighSuppress the address immediately
    bounce (soft)Temporary failure — mailbox full, greylistedHighCount consecutive failures; suppress after a threshold
    deferredDelivery delayed, retries in progressHighLog only; do not alert users
    openTracking pixel loadedLowTreat as a weak signal, never as proof of reading
    clickA tracked link was requestedMediumAttribute intent; watch for security scanners
    spam complaintRecipient hit the spam buttonHighSuppress permanently and investigate the send
    unsubscribeOpt-out via link or list-unsubscribe headerHighUpdate preferences immediately

    Notice the reliability column. Open events are polluted by image proxies that prefetch pixels, and click events are polluted by corporate security appliances that visit every link in an inbound message before the human sees it. Build your logic accordingly — bounces and complaints are actionable, opens are directional at best.

    What a payload looks like

    Payload shapes vary by provider, but nearly all include the same core fields:

    • event — the event name, e.g. bounce
    • message_id — the identifier tying this event back to a specific send
    • recipient — the address the event concerns
    • timestamp — when the event occurred, usually Unix epoch in UTC
    • reason or smtp_response — the raw response from the receiving server, invaluable for diagnosing bounces
    • custom metadata — any key-value pairs you attached at send time

    That last field is the one people skip and later regret. Attach your own internal identifiers — user ID, order ID, campaign ID — when you send. Otherwise every webhook handler starts with a database lookup to figure out what the message was about, and at volume that lookup becomes your bottleneck.

    Also expect batching. Many providers POST an array of events rather than one object, so write your handler to accept a list even if today it usually receives a single item.

    Building an endpoint that does not lose events

    The single most important design rule: acknowledge fast, process later.

    1. Verify the signature on the raw request body before parsing.
    2. Write the raw payload to a queue or table exactly as received.
    3. Return 200 immediately — ideally in well under a second.
    4. Process asynchronously in a worker that can fail and retry without affecting the HTTP response.

    If you do database writes, send emails, or call third-party APIs inside the request handler, you will eventually time out during a traffic spike. The provider will interpret the timeout as a failure, retry, and you will get duplicates on top of an already-struggling endpoint.

    Return codes matter. A 2xx means "received." A 4xx tells most providers to stop retrying, which is what you want for a genuinely malformed payload. A 5xx tells them to retry, which is correct when your database is down. Never return 200 to hide an internal error — you will silently lose events with no way to recover them.

    Securing the endpoint

    Your webhook URL is a public HTTP endpoint that writes to your database based on unauthenticated input. Treat it accordingly.

    Verify signatures on every request. Providers sign payloads with a shared secret, typically an HMAC over the raw body plus a timestamp. Compute the expected value and compare using a constant-time comparison function. Verify against the raw bytes, not a re-serialized object — JSON round-tripping changes whitespace and key order and will break the check.

    Reject stale timestamps. If the signed timestamp is more than a few minutes old, drop the request. This blocks replay of a captured payload.

    Use HTTPS and a non-guessable path. Obscurity is not security, but it cuts noise from opportunistic scanners.

    Rate limit and cap body size. An unbounded endpoint is a denial-of-service target.

    If you are sending through an authenticated relay, the same secret-handling discipline applies to your credentials — our SMTP relay setup guide covers ports, authentication, and credential rotation for the sending side of the same pipeline.

    Retries, duplicates, and out-of-order delivery

    Webhook delivery is at-least-once, not exactly-once. Assume every one of these will happen in production:

    Duplicates. The same event arrives twice because your 200 was slow. Make handlers idempotent — key on a combination of message ID, event type, and timestamp, and ignore anything you have already recorded.

    Out-of-order arrival. A delivered event can arrive after an open. Never write state machines that assume sequence. Store events as an append-only log and derive current status from the log rather than overwriting a single status column.

    Retry storms. If your endpoint is down for an hour, the provider's backlog arrives all at once when you recover. Your queue absorbs this; a synchronous handler will not.

    Permanent disabling. Most providers stop sending after repeated failures over some window. Alert on webhook error rate, not just on application errors, or you will discover the outage days later through missing data.

    Acting on the events that matter

    Suppression is the non-negotiable one. Hard bounces and spam complaints must remove the address from future sends automatically, with no human in the loop. Continuing to mail addresses that hard-bounced is one of the fastest ways to damage sending reputation, and complaint rates above roughly 0.1-0.3% put you in dangerous territory with major mailbox providers.

    For soft bounces, count consecutive failures per address and suppress after three to five in a row across separate sends. A single mailbox-full response means nothing.

    For clicks, filter obvious machine traffic before attributing intent — clicks arriving within a second or two of delivery, multiple distinct links clicked in the same instant, or user agents from known security vendors. For opens, resist building anything load-bearing on top of them. Use them for trend lines, never for triggering messages that would embarrass you if the "open" was a proxy prefetch.

    Frequently asked questions

    What response code should my webhook endpoint return?

    Return 200 as fast as possible after verifying the signature and persisting the raw payload. Use 5xx only when you genuinely want a retry, and 4xx for payloads that will never be processable.

    Why am I receiving duplicate webhook events?

    Almost always because your endpoint responded slowly or errored after doing partial work, triggering a provider retry. Make handlers idempotent by deduplicating on message ID plus event type plus timestamp.

    Can I trust open events from webhooks?

    Only loosely. Image proxies prefetch tracking pixels and privacy features inflate counts, so opens are useful for comparing sends against each other but should not drive automation or billing logic.

    How do I test webhooks locally?

    Use a tunneling tool to expose your local server, then trigger real test sends. Capture a few production payloads and replay them against your handler so your test fixtures reflect actual provider output.

    What happens if my endpoint is down?

    Providers retry with backoff for a bounded window, then typically disable the webhook. Monitor delivery failure rates and provide a way to backfill from the provider's event API after an extended outage.

    Need transactional sending with clean event data behind it? IGSendMail handles SPF, DKIM, and DMARC automatically and delivers 99% inbox placement from $19/mo. Get started with IGSendMail.

    Enjoyed this article?

    Get email marketing tips delivered to your inbox every week.