Webhooks explained: a webhook is an HTTP request that one application sends to another application when a defined event occurs. Instead of your software repeatedly asking whether something changed, the service that knows about the change pushes a notification to an endpoint you control.

For developers, webhooks are one of the simplest ways to build event-driven integrations. They are used for payment updates, source-control events, ecommerce orders, account changes, and transactional email events such as accepted, delivered, bounced, complained, opened, and clicked. They are simple in concept, but a reliable webhook implementation requires careful work around security, duplicate delivery, retries, ordering, and observability.

What is a webhook?

A webhook is commonly described as an HTTP callback. An application, called the webhook producer or sender, makes an outbound HTTP request to a URL supplied by another application, called the consumer, receiver, or listener.

The request is triggered by an event. For example:

  • A customer completes checkout.
  • A Git repository receives a push.
  • A subscription invoice is paid.
  • An email provider records that a recipient server accepted a message.
  • A mailbox provider reports a spam complaint.

The receiver exposes a public endpoint such as:

https://api.example.com/webhooks/email-events

When the configured event happens, the producer usually sends an HTTP POST request with a JSON payload. The payload identifies the event, provides relevant data, and often includes an event ID and timestamp. The receiver processes the request and replies with an HTTP status code.

A webhook is not a separate Internet protocol in the way SMTP or DNS is. It is a design pattern built on ordinary HTTP semantics: a request, headers, a body, and a response. HTTP status codes are grouped into classes, including successful 2xx responses, client-error 4xx responses, and server-error 5xx responses. (rfc-editor.org)

Webhooks versus APIs versus polling

Webhooks and APIs are often discussed as opposites, but they solve different directions of communication.

An API lets your application actively request or change information. For instance, your app might call an email API to send a password-reset message, list suppressions, or retrieve a message record. A webhook lets the other service proactively tell your app that something happened after the API request.

Polling: repeatedly asking for changes

With polling, your application calls an endpoint at intervals:

GET /v1/messages?updated_since=2026-08-14T12:00:00Z

Polling is reasonable when updates are infrequent, real-time behavior is unnecessary, or the external service offers no webhooks. But it creates a trade-off: short intervals produce faster updates but more requests; long intervals reduce requests but delay your data.

Imagine checking every minute for an email bounce. You might make 1,440 requests a day even if no recipient has bounced. If ten separate systems poll on similar schedules, the unnecessary traffic and operational complexity compound.

Webhooks: event-driven delivery

With a webhook, the provider sends a request only after an event occurs:

POST /webhooks/email-events HTTP/1.1
Host: api.example.com
Content-Type: application/json

{"id":"evt_01J...","type":"email.bounced","created_at":"2026-08-14T12:03:11Z"}

That can reduce latency and eliminate routine “anything new?” requests. However, the receiving system must be available to accept inbound requests, and it must safely handle delivery behavior that is inherently distributed and unreliable.

A useful rule of thumb

Use an API when you need to ask or command. Use a webhook when you need to be notified. Production integrations frequently use both:

  1. Your application sends an email through an SMTP relay or REST API.
  2. The provider returns an immediate submission result, such as a message ID.
  3. Later, the provider posts webhook events as the message is processed and reaches recipient infrastructure.
  4. Your application stores those events, updates support tooling, and changes future sending decisions when appropriate.

How a webhook works, step by step

The lifecycle is straightforward, even though the details matter.

1. You register a destination URL

You configure a URL that the provider may call. The URL normally needs to be publicly reachable over HTTPS. A typical endpoint might be:

https://hooks.example.com/providers/email

Your DNS must resolve the hostname to the infrastructure that serves the endpoint. For example, an A record can map a hostname to an IPv4 address:

hooks.example.com. 300 IN A 203.0.113.42

Or a CNAME record can alias a subdomain to another hostname managed by a platform or load balancer:

hooks.example.com. 300 IN CNAME app-gateway.example-host.net.

The trailing dot represents a fully qualified domain name in zone-file syntax. In a DNS provider dashboard, the provider may add the zone automatically, so you might enter only hooks as the host and omit the final dot from the target. The exact fields vary by DNS provider; verify the rendered record after saving it. A DNS lookup tool such as MXToolbox, dig, or nslookup can confirm the published result.

2. An event occurs

The sender observes an event in its own system. In an email context, examples include a message being accepted for sending, deferred, delivered, bounced, marked as spam, opened, clicked, or unsubscribed.

Importantly, each provider defines its own event taxonomy and timing. “Delivered” often means that a recipient mail server accepted the message, not that a human saw it in an inbox. Open tracking also depends on a tracked image being loaded, so privacy features, image blocking, and plain-text email can prevent an open from being observed.

3. The sender builds a request

Most webhook requests contain:

  • An HTTP method, usually POST.
  • A destination URL.
  • Headers such as Content-Type: application/json.
  • Authentication or signature headers.
  • A JSON request body.
  • Sometimes a delivery attempt number, event ID, or timestamp.

A generic email-delivery payload could look like this:

{
  "id": "evt_7d4c0a8f",
  "type": "email.delivered",
  "created_at": "2026-08-14T12:03:11Z",
  "data": {
    "message_id": "msg_b91f55f2",
    "recipient": "alex@example.net",
    "provider_message_id": "<abc.123@example-mail.net>",
    "occurred_at": "2026-08-14T12:03:10Z"
  }
}

Do not assume every provider uses these field names. A webhook contract is provider-specific unless both sides explicitly adopt a common specification. Treat the provider’s documentation as authoritative for event names, headers, signature construction, retry policy, payload schema, and versioning.

4. Your endpoint validates and acknowledges it

Your endpoint receives the request, authenticates it, records enough information to process it safely, then returns a response. A successful acknowledgement is commonly 200 OK, 202 Accepted, or 204 No Content.

202 Accepted is especially useful for webhook handlers because it communicates that your server accepted the event for asynchronous processing. It does not mean your downstream work is complete. It means the ingress endpoint has durably accepted responsibility for it.

5. The sender may retry

If the sender cannot connect, times out, or receives a response it treats as unsuccessful, it may retry later. Retry schedules vary widely. Some systems retry for minutes; others retry for hours or days. That variation is why a webhook receiver should be designed for at-least-once delivery rather than assuming exactly one request per event.

A concrete email webhook example

Transactional email is a natural webhook use case because sending and final outcomes happen at different times.

Suppose your application sends a receipt through a provider’s REST API or SMTP relay. The immediate response can confirm that the provider accepted the request. That is useful, but it is not final delivery confirmation.

The provider may then attempt SMTP delivery to the recipient domain. SMTP uses three-digit replies. A 250 reply is a successful completion; 421 and 450 are examples of transient conditions that can lead to later retry; 550 is commonly used for a permanent failure such as an unknown mailbox, depending on the recipient system’s exact response. (datatracker.ietf.org)

A simplified chain looks like this:

  1. Your app submits receipt-1048 to the email provider.
  2. The provider emits an email.accepted or equivalent event.
  3. The provider contacts the recipient’s mail infrastructure over SMTP.
  4. The recipient system either accepts, temporarily defers, or rejects the message.
  5. The provider posts a webhook event representing the observed outcome.
  6. Your application records the event against the customer, invoice, or support case.

Why the distinction matters

A request accepted by an email API means the provider accepted your instruction. It does not prove the message reached a receiving mail server. A receiving server’s SMTP acceptance does not prove the email entered the primary inbox. And an open event does not prove the recipient read or understood the email.

These stages should be modeled separately in your application. Conflating them creates misleading customer-facing statuses and makes troubleshooting harder. A useful state model might distinguish submitted, accepted, delivered, deferred, bounced, complained, and unsubscribed rather than treating every non-error as “sent.”

Before sending to a newly collected address, an email address verification tool can help catch malformed or risky inputs. It is not a replacement for bounce and complaint handling, because deliverability changes over time and some mailbox states can only be observed during actual delivery attempts.

What your webhook endpoint should return

The response code is part of the contract. It tells the sender what happened to its delivery attempt.

Successful responses: 200, 202, and 204

Return a 2xx response only after you have completed the minimum safe action needed to avoid losing the event. In most architectures, that means one of the following:

  • Persist the raw event and its ID in a database.
  • Publish the event to a durable queue.
  • Store it in an append-only event log.

Then process the expensive business logic asynchronously.

Returning 200 OK before durable storage risks event loss if your server crashes a millisecond later. Returning 202 Accepted after writing to a queue is often a clean expression of the architecture: the event is accepted, but a worker still needs to process it.

Client-error responses: 400, 401, 403, and 404

Use 400 Bad Request when the payload is malformed or fails basic validation. Use 401 Unauthorized or 403 Forbidden when authentication fails, depending on your authentication design. A 404 Not Found indicates that the route does not exist, which may point to a bad endpoint URL or a deployment-routing problem.

Be cautious: some webhook providers retry most non-2xx responses, while others distinguish between temporary and permanent failures. Invalid signatures should normally be rejected, but you should also alert on repeated failures because they can indicate a configuration mismatch, secret rotation problem, or attack.

Server-error responses: 500, 502, 503, and 504

Return 5xx only for conditions that are genuinely temporary on your side: a database outage, queue failure, overloaded service, bad upstream dependency, or deployment incident. These status codes generally tell a sender that retrying later may work.

Do not return 500 for an event you have already processed successfully. That creates a duplicate on the next attempt. Conversely, do not return 200 merely to stop retries when your storage layer has failed; that silently discards data.

Webhook security: verify before you trust

A public webhook URL is an Internet-facing write path into your system. Anyone can discover, guess, or send traffic to a URL. The URL itself is not sufficient proof that the request came from the provider you expect.

A sound webhook security model usually combines HTTPS, origin authentication, integrity checks, replay protection, strict input handling, and operational monitoring. OWASP’s webhook guidance emphasizes that webhook receivers accept inbound HTTP traffic and need deliberate hardening across the delivery pipeline. (github.com)

Use HTTPS

Configure an https:// endpoint with a valid TLS certificate. HTTPS encrypts traffic in transit and helps prevent network observers from reading or modifying payloads.

HTTPS alone does not establish that the sender is your provider. It protects the transport channel, not the application-level identity of every request. You still need to validate the provider’s authentication mechanism.

Verify HMAC signatures using the raw request body

A common approach is HMAC signing. The sender and receiver share a secret. The sender computes a cryptographic signature over a precisely defined value, often the raw request body or a timestamp plus raw body. The receiver independently computes the expected signature and compares it to the signature header.

Conceptually:

expected = HMAC_SHA256(signing_secret, raw_request_body)

If the sender uses a timestamped signing string, it may instead be conceptually similar to:

expected = HMAC_SHA256(signing_secret, timestamp + "." + raw_request_body)

The exact bytes and formatting are critical. Do not parse JSON, reserialize it, trim whitespace, or change character encoding before signature verification unless the provider’s specification explicitly says to do so. Middleware that parses the request too early can make a valid signature fail because the raw bytes are no longer available. Shopify’s webhook verification guidance specifically calls out raw-body handling as a common source of HMAC verification errors. (shopify.dev)

Use a constant-time comparison function rather than a normal string comparison when your language provides one. In Node.js, crypto.timingSafeEqual is commonly used after confirming that both buffers have equal length. Keep signing secrets in a secrets manager or environment configuration, never in source control or browser-delivered code.

Prevent replay attacks

A valid request can be captured and replayed. To reduce that risk:

  • Validate a signed timestamp when the provider supplies one.
  • Reject events older than a narrow acceptable window, allowing for expected clock skew.
  • Store processed event IDs and reject or ignore duplicates.
  • Rotate signing secrets carefully, accepting old and new secrets during a defined transition where the provider supports it.

An event ID is useful for deduplication, but it is not a substitute for signature verification. An attacker can invent an event ID. A signature is useful for authenticity, but it does not by itself make event processing idempotent.

Validate schema and limit exposure

After verifying the signature, validate the payload shape. Check required fields, types, timestamps, expected event names, and known version values. Place limits on request size and parsing depth. Avoid logging passwords, signing headers, full email content, authorization values, or sensitive personal data.

IP allowlists can be an additional control if a provider publishes stable source ranges, but do not treat them as your only defense. Cloud services can change egress architecture, proxies can alter source addresses, and allowlist maintenance can become a reliability risk. Signed requests are generally the stronger application-level control.

Reliability: assume at-least-once delivery

The most important implementation rule is this: a webhook can arrive more than once.

A sender may time out waiting for your response after your application has already persisted the event. Your server may send a response that is lost in transit. The sender cannot safely know whether you completed the work, so retrying is reasonable.

That creates at-least-once delivery: you should expect one or more attempts for an event, not exactly one.

Build idempotent processing

Idempotency means processing the same event repeatedly produces the same final result as processing it once.

For a webhook event with ID evt_7d4c0a8f, create a durable record keyed by a stable identifier. The database insert should be protected by a unique constraint or equivalent atomic operation.

CREATE TABLE webhook_events (
  provider_event_id TEXT PRIMARY KEY,
  event_type TEXT NOT NULL,
  received_at TIMESTAMPTZ NOT NULL,
  payload JSONB NOT NULL,
  processed_at TIMESTAMPTZ
);

When the event arrives:

  1. Verify the signature.
  2. Insert the event ID and raw payload.
  3. If the unique insert succeeds, enqueue work.
  4. If it conflicts with the existing event ID, acknowledge it without repeating side effects.

The exact database syntax differs across systems, but the principle does not: deduplication must happen atomically. A simple “check whether it exists, then insert” can race when two delivery attempts arrive at nearly the same time.

Do not assume event order

Network delays, retries, regional queues, and independent processing pipelines mean event order is not guaranteed. A delivered event might arrive before an earlier accepted event. A retry of an old event might arrive after a newer event.

Design your state transitions accordingly. Store the event timestamp separately from receive time, and apply rules such as “do not let an older event overwrite a newer terminal state.” When event types have a natural lifecycle, define precedence explicitly. For example, a valid complaint should not be overwritten by a delayed delivered event.

Acknowledge quickly, process later

Webhook senders often impose short request timeouts. Do not synchronously call slow third-party APIs, render reports, send follow-up email, or run a large database migration inside the request handler.

A robust pattern is:

Webhook request
  -> signature verification
  -> durable event store or queue
  -> 2xx response
  -> asynchronous worker
  -> business logic, metrics, retries, alerts

This reduces timeout risk and separates the availability of your public webhook endpoint from the availability of every downstream system.

A practical handler design

The following pseudocode shows the intended ordering. It is deliberately generic because signature headers, hash algorithms, and event schemas differ by provider.

async function receiveWebhook(request) {
  const rawBody = await request.readRawBody();
  const signature = request.headers.get("provider-signature");
  const timestamp = request.headers.get("provider-timestamp");

  if (!isFreshTimestamp(timestamp, 300)) {
    return response(400, "stale webhook");
  }

  if (!verifySignature(rawBody, signature, timestamp, process.env.WEBHOOK_SECRET)) {
    return response(401, "invalid signature");
  }

  const event = JSON.parse(rawBody);
  validateEventSchema(event);

  const inserted = await insertEventIfNew({
    id: event.id,
    type: event.type,
    occurredAt: event.created_at,
    rawPayload: rawBody
  });

  if (inserted) {
    await enqueue("process-webhook-event", { eventId: event.id });
  }

  return response(204);
}

The worker can load the event from storage, apply business rules, mark it processed, and retry its own downstream failures separately. This architecture is easier to inspect because you can distinguish “the provider could not deliver the webhook” from “we accepted it but our internal worker failed later.”

Email-specific webhook design decisions

Email events affect customer communication, reputation, compliance, and support operations. Treat them as operational signals, not merely analytics.

Bounces and suppressions

A hard bounce typically indicates a permanent delivery problem, such as an invalid or nonexistent mailbox. A soft bounce or deferral indicates a temporary problem, such as mailbox limits, recipient-server throttling, or temporary infrastructure trouble. Providers may use their own classifications, so consume the structured fields and SMTP diagnostic data they supply instead of classifying only from an event name.

When a permanent failure is confirmed, stop repeatedly sending to that address unless you have a clear remediation path. Continuing to attempt delivery to known-bad recipients wastes sending capacity and can harm reputation. Record the reason, the event timestamp, and the message category so support staff can explain what happened.

Complaints and unsubscribes

Spam complaints should trigger fast action. At minimum, suppress promotional email to the complainant. Depending on your legal basis, product design, and applicable rules, you may also need to distinguish marketing consent from necessary transactional communications.

Unsubscribe events are similarly important. They should be processed idempotently and quickly. If the same unsubscribe event arrives twice, the final state should remain unsubscribed without generating a duplicate confirmation or error.

Opens and clicks

Open and click webhooks can support engagement reporting, but they are not ground truth. Opens rely on tracking pixels and can be affected by image proxies, privacy protections, and blocked images. Clicks can be triggered by security scanners or link-preview systems. Use these events as signals, not as the only basis for security-sensitive or customer-impacting decisions.

Authentication records are related, but not webhook configuration

Webhooks do not require email-authentication DNS records. However, email delivery events are more meaningful when your sending domain is properly authenticated. Typical DNS records include SPF, DKIM, and DMARC.

An SPF record is published as TXT and commonly resembles:

example.com. 3600 IN TXT "v=spf1 include:spf.example-mail-provider.net -all"

A DMARC record is also TXT and is published at _dmarc:

_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com"

DKIM records are published beneath a selector chosen by the sending system. A simplified example is:

s1._domainkey.example.com. 3600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqh..."

These are examples, not values to copy blindly. Your email provider supplies the exact DKIM selector, hostname, and public key or CNAME target required for its service. Tools such as MXToolbox and mail-tester.com can help inspect published records and test message authentication, but provider documentation remains the source of truth for the required records.

Debugging webhook failures

Webhook bugs often involve the boundary between systems, so debugging needs evidence from both sides.

Start with a delivery log from the sending provider when available. Note the event ID, destination URL, timestamp, HTTP status returned, response body if recorded, attempt count, and request headers. Then correlate those details with your application logs.

Common symptoms and likely causes

  • No webhook arrives: wrong URL, missing DNS record, inaccessible endpoint, firewall rule, TLS certificate issue, event not enabled, or a provider-side delivery failure.
  • Every request returns 401 or 403: wrong signing secret, using parsed rather than raw bytes, incorrect signature algorithm, missing timestamp in the signing string, or a secret rotation mismatch.
  • Duplicate processing: no idempotency key, non-atomic duplicate checks, or external side effects occurring before event persistence.
  • Retries never stop: endpoint returns 500, responds after the sender timeout, or returns a non-2xx status after successful work.
  • Events look out of order: assuming arrival order equals occurrence order, ignoring retries, or applying old data over newer data.
  • Works locally but fails in production: local tunnel is not running, reverse proxy strips headers, body parser changes the raw payload, production DNS points elsewhere, or TLS is misconfigured.

Use a request-inspection tool during development only if it is appropriate for the data involved. Webhook payloads can contain email addresses, order details, and other personal data, so avoid forwarding production events into tools that are not approved for that information.

For production observability, track at least these metrics:

  • Webhook request count by provider, event type, and response code.
  • Signature-validation failure count.
  • Processing latency from occurred_at to durable receipt and from receipt to completed work.
  • Duplicate-event rate.
  • Queue depth and worker failure rate.
  • Dead-lettered events and the age of the oldest unprocessed event.

When webhooks are not the right tool

Webhooks are excellent for prompt notifications, but they do not solve every integration need.

Use an API query, scheduled export, or polling job when you need historical backfills, a complete current-state snapshot, or reconciliation after an outage. A webhook can be missed because of a configuration mistake before you deployed the endpoint, and an event stream may not contain every field needed for reporting.

A resilient system often combines webhooks with periodic reconciliation:

  1. Webhooks keep application state fresh in near real time.
  2. A scheduled job queries the provider’s API or export for recent records.
  3. The job identifies missing events or inconsistent state.
  4. Your system repairs or flags discrepancies.

For high-value workflows, this is more trustworthy than relying on either webhooks or polling alone.

Webhook implementation checklist

Before putting a webhook endpoint into production, confirm the following:

  • The endpoint uses HTTPS and a valid certificate.
  • DNS resolves the endpoint hostname to the intended service.
  • The handler reads the provider-required raw body before parsing JSON.
  • Signature verification follows the provider’s exact specification.
  • Signing secrets are stored securely and can be rotated.
  • Timestamp validation or another replay defense is enabled when supported.
  • Event IDs are stored with an atomic uniqueness guarantee.
  • The endpoint returns a 2xx response only after durable acceptance.
  • Slow work is moved to a queue or background worker.
  • Processing is idempotent and does not depend on arrival order.
  • Logs correlate provider event IDs, internal job IDs, and message IDs.
  • Alerts exist for delivery failures, signature failures, and growing queues.
  • A reconciliation plan exists for missed or delayed events.

If you are choosing an email platform, compare not only sending methods such as REST API and SMTP relay, but also the event model, signing method, retention period, retry behavior, suppression handling, and the operational detail available in delivery logs. The email API setup guides and reference should make those behaviors explicit rather than leaving them to guesswork.

Conclusion

Webhooks are event-driven HTTP callbacks: one service tells another that something happened. They reduce polling and make integrations more responsive, but they should be treated as untrusted, retryable messages crossing a network boundary.

The durable implementation pattern is simple: verify the request, store or enqueue it atomically, acknowledge quickly, and process it idempotently in the background. For email systems, model each stage honestly, from API acceptance through SMTP outcomes to engagement signals, and let bounce, complaint, and unsubscribe events inform your sending decisions.

FAQ

Are webhooks the same as APIs?

No. An API is a general interface your application calls to request data or trigger actions. A webhook is an outbound HTTP notification a service sends to your endpoint when an event occurs. Most integrations use both.

Why does my webhook arrive twice?

Duplicate delivery is normal in an at-least-once system. A sender may retry when it cannot confirm that your endpoint received a prior attempt. Store a stable event ID with a unique constraint and make downstream actions idempotent.

Should a webhook endpoint return 200 or 202?

Either can be correct. Return a 2xx status after you have durably accepted the event. 200 OK is common for completed lightweight handling; 202 Accepted is useful when you have queued the event for asynchronous work; 204 No Content is suitable when no response body is needed.

Can I secure a webhook with a secret URL alone?

No. An obscure URL is not reliable authentication. Use HTTPS and verify the provider’s signed request, usually with HMAC over the raw request bytes. Add replay protection and event-ID deduplication.

Does an email delivered webhook mean the recipient read the email?

No. It normally indicates that recipient mail infrastructure accepted the message. An open event may provide an engagement signal, but opens are affected by image loading, privacy features, and tracking limitations.