An email webhook is the practical bridge between your email provider and your application: when an email event happens, the provider sends an HTTPS POST request to an endpoint you control. Set it up well and you can suppress bounced addresses, update a CRM, process customer replies, and retain an independent event trail without polling for changes. (twilio.com)
What an email webhook is—and what it is not
A webhook is an event-driven HTTP callback. Instead of your application repeatedly asking an email provider, “Did this message bounce yet?” the provider sends your application an event when it processes a delivery, bounce, complaint, click, reply, or another configured signal. Providers commonly send JSON over HTTPS, although inbound-email products can use other formats such as multipart/form-data. (resend.com)
There are two email webhook jobs that are easy to confuse:
- Outbound event webhooks report what happened after your system sent an email. Typical events include
delivered,bounced,complained,opened,clicked, andunsubscribed. - Inbound email webhooks turn received mail into an HTTP request so your app can process a support message, a reply, an attachment, or an email-based command.
Twilio SendGrid, for example, separates its Event Webhook for outbound tracking from Inbound Parse for received email. Its Event Webhook groups events into deliverability events, such as delivered and bounced, and engagement events, such as opens and clicks. (twilio.com)
An email webhook is not an email-sending API. You still send email through SMTP or an email API. It is also not automatically a mailbox: for inbound use cases, a provider must first receive the message for a domain or provider-managed address, then forward data about that message to your endpoint.
Choose the email webhook you actually need
Start with the business action you need to automate, not the list of all events a provider offers. Receiving every possible event creates more data, more duplicate-handling work, and more chances to build an unreliable workflow around a weak signal.
Delivery and bounce webhooks
Use these for operational email hygiene. A delivery event can update a message timeline or indicate that the receiving mail system accepted a message. A hard-bounce or permanent-failure signal should normally stop future sends to that address until a deliberate re-verification or user correction process occurs.
A sensible workflow is:
- Store the provider message ID when you send the email.
- Map each incoming event to that message ID and recipient.
- Record the raw event and a normalized event type.
- Suppress or flag recipients based on your provider’s bounce and complaint semantics.
- Alert when a transactional flow suddenly produces unusual failure events.
Do not treat an event called delivered as proof that a human saw the email or that it landed in the primary inbox. It is an event in the provider’s delivery pipeline, and exact definitions differ by provider. Keep the original provider event alongside your normalized status so you can investigate later.
Complaint and unsubscribe webhooks
Complaint and unsubscribe events are compliance and reputation signals, not marketing analytics. When a provider tells you a recipient complained or unsubscribed, update the appropriate suppression or consent state promptly. Do not make such updates depend on a slow background report or a manual spreadsheet import.
Separate the concepts in your data model. A recipient may be globally unsubscribed from promotional mail but still eligible for a password reset or legally required service notification, depending on your product, message classification, and applicable rules. Your application should have explicit categories such as marketing, product_updates, and transactional rather than a single ambiguous email_allowed boolean.
Open and click webhooks
Open and click events can support product analytics, lead routing, or campaign reporting, but should not be the sole source of truth for a business-critical decision. Treat them as engagement indicators, not identity or intent verification.
For example, a click on an account-verification link should be validated by the signed token in the link itself. Do not unlock an account merely because an email webhook says a click occurred. The webhook can enrich your audit trail after the application has securely completed the action.
Inbound email webhooks
Inbound webhooks are for workflows where email becomes user input. Common examples include:
- Converting
support@messages into tickets. - Letting customers reply to a transactional notification.
- Receiving documents or photos as attachments.
- Creating an item in a workflow when an approved sender emails a special address.
- Processing replies such as
approve,decline, or a comment on a shared document.
Postmark describes inbound processing as accepting messages at an inbound address or forwarding domain and then posting the parsed email as JSON to your URL. SendGrid’s Inbound Parse can deliver parsed content, attachments, and headers to your endpoint. (postmarkapp.com)
The email webhook architecture to build
A durable implementation has more than a public route and a console.log. Build four distinct stages:
- Receive the HTTPS request and preserve the unmodified payload needed for verification.
- Verify the provider signature, timestamp, and any replay protections before trusting the content.
- Persist and acknowledge the event quickly: save it durably, deduplicate it, and return success.
- Process asynchronously: update a CRM, change a subscription state, download an attachment, send an alert, or trigger another workflow from a queue or worker.
This separation is important because webhook systems retry when they cannot tell whether you handled a request. Resend explicitly documents at-least-once delivery and says events can occasionally arrive more than once, including when your server processed the event but its acknowledgement was lost. Its svix-id header is intended to support duplicate handling. (resend.com)
Your endpoint should therefore be idempotent. In plain English, processing the same webhook twice must leave the system in the same correct end state as processing it once.
A minimal event table might include:
| Field | Why it exists |
|---|---|
provider | Lets one system receive events from multiple providers. |
delivery_id | The provider’s unique delivery identifier, used for deduplication. |
event_type | The original provider event, such as email.bounced. |
provider_message_id | Connects an event to a sent message or inbound object. |
received_at | Helps diagnose delays and retry patterns. |
payload_json | Preserves the original parsed event for audits and reprocessing. |
processing_status | Tracks queued, processed, ignored, and failed work. |
Create a database uniqueness constraint on a stable delivery ID, such as (provider, delivery_id). Do not rely on a recipient email address plus timestamp: one campaign can generate several valid events for the same person, and timestamps can collide or differ between retries.
Set up an outbound email webhook step by step
The exact dashboard labels and event names are provider-specific, but the setup sequence is stable.
1. Create a narrow, public HTTPS endpoint
Use a dedicated route such as:
POST https://api.example.com/webhooks/email/resend
Keep it separate from browser-facing application routes. A webhook endpoint does not need a user session, cookies, or a login page. It should accept only the HTTP method and content type your provider documents.
For local development, expose a temporary HTTPS URL with a tunnel such as ngrok or use a provider’s local-listening CLI where available. Resend documents both public tunneling tools and a CLI listener for local webhook development. (resend.com)
Do not leave a development tunnel configured in production. Configure a production hostname with a valid TLS certificate, access logs, alerts, and an owner on your team.
2. Select only useful event types
For a transactional application, start with:
email.delivered
email.bounced
email.complained
email.delivery_delayed
The exact labels vary. Resend uses names such as email.bounced, while SendGrid allows event selection as part of Event Webhook configuration. (resend.com)
Add opens and clicks only if you have a documented use case. Add subscription changes if your email provider is the authoritative source for subscriber status. Add inbound events only after you have a secure plan for untrusted email content and attachments.
3. Save the signing secret or public key outside source control
Webhook authentication is not optional. A random internet client can send an HTTP POST request to a public URL. Without verification, an attacker could submit a fake bounce and cause your system to suppress a real customer, forge an unsubscribe, trigger an internal workflow, or poison analytics.
Provider schemes differ:
- Resend, which uses Svix-style webhooks, provides a signing secret and signature headers including
svix-id,svix-timestamp, andsvix-signature. Verification is performed against the raw request body. (resend.com) - Mailgun includes a
timestamp,token, and HMAC-SHA256 signature in its webhook payload. Its documented verification method signs the concatenation of timestamp and token with the Webhook Signing Key. (documentation.mailgun.com) - Twilio SendGrid Signed Event Webhook uses a private/public key pair and puts a signature in the
X-Twilio-Email-Event-Webhook-Signatureheader. (twilio.com)
Store the secret in a managed secret store or deployment environment variable, for example RESEND_WEBHOOK_SECRET. Never put it in a frontend bundle, a client-side environment variable, a git repository, or a ticket comment.
4. Verify first, parse second where the scheme requires raw bytes
This is the webhook bug that catches experienced developers: JSON parsing can change whitespace, Unicode escaping, property ordering, or line endings. A signature covers a specific byte sequence, so reconstructing JSON after parsing can fail verification even if the object looks identical.
Resend requires the raw request body for verification, and Svix documents that even a small body change produces a different signature. (resend.com)
In Express, do not put express.json() ahead of a webhook route that needs the raw body. In Next.js route handlers, use await request.text() for that route. In serverless platforms, read the provider-specific framework documentation to ensure a body parser does not consume or transform the payload first.
5. Deduplicate before business processing
After a verified request arrives, insert its delivery ID into your database. If the uniqueness constraint reports that it already exists, return a successful response without repeating side effects.
This means a duplicate bounce event does not repeatedly create CRM tasks, send multiple Slack alerts, or attempt to delete the same subscriber record. It also makes manual provider replays safe: Resend supports replaying webhook events for missed events or updated handler code. (resend.com)
6. Return success quickly and process work in the background
A webhook response means, “I safely accepted this delivery,” not necessarily, “Every downstream task is complete.” Put expensive work—database joins, attachment scanning, API calls, AI classification, CRM writes, and notification fan-out—on a queue after you durably store the event.
A 2xx response is conventionally the acknowledgement that a webhook was accepted. Svix documents 2xx responses as successful processing signals, and Mailgun documents that it does not retry after a 200 response. (docs.svix.com)
Return a non-2xx response only when you want the provider’s retry behavior, and know that behavior before relying on it. Mailgun, for instance, treats 406 Not Acceptable as rejected without retry and retries most other non-200 webhook responses on its documented schedule. (documentation.mailgun.com)
Worked example: receive and process a Resend email webhook
This example uses a Next.js App Router endpoint and Resend’s documented verification method. It handles a bounce event, deduplicates by the Svix message ID, saves a normalized record, and queues the actual suppression work.
Install the provider package used by your application:
npm install resend
Create app/api/webhooks/resend/route.ts:
import { NextResponse } from "next/server";
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(request: Request) {
const rawBody = await request.text();
const headers = {
id: request.headers.get("svix-id") ?? "",
timestamp: request.headers.get("svix-timestamp") ?? "",
signature: request.headers.get("svix-signature") ?? "",
};
let event: any;
try {
event = resend.webhooks.verify({
payload: rawBody,
headers,
webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,
});
} catch {
return new NextResponse("Invalid webhook signature", { status: 400 });
}
const deliveryId = headers.id;
// Pseudocode: enforce a UNIQUE(provider, delivery_id) database index.
const wasInserted = await db.emailWebhookEvents.insertIfAbsent({
provider: "resend",
delivery_id: deliveryId,
event_type: event.type,
provider_message_id: event.data?.email_id ?? null,
recipient: event.data?.to?.[0] ?? null,
payload_json: event,
received_at: new Date(),
});
if (!wasInserted) {
return NextResponse.json({ ok: true, duplicate: true });
}
await queue.publish("process-email-webhook", {
provider: "resend",
deliveryId,
});
return NextResponse.json({ ok: true });
}
The background worker can make the business decision:
if (event.event_type === "email.bounced") {
await recipients.markUndeliverable({
email: event.recipient,
source: "resend_webhook",
});
}
The important implementation details are not the names of db and queue; those are placeholders for your database and job system. The essential parts are reading request.text() before verification, calling the provider verifier with the signing headers, storing the unique delivery ID, and returning a 2xx response only after the event is durable.
Resend’s documented verifier takes the raw payload and svix-id, svix-timestamp, and svix-signature headers, then throws when the delivery is invalid. (resend.com)
To test it:
- Deploy the endpoint to a public HTTPS URL.
- In the provider dashboard, create a webhook for that URL and select
email.bouncedplus one easy-to-trigger event such asemail.delivered. - Send a test email through your normal sending flow.
- Confirm a new row appears in
email_webhook_eventswith the expecteddelivery_idand event type. - Replay the same event from the provider, if supported, and confirm your endpoint returns success but does not create a second row or repeat the business action.
Your sending code and email-webhook handler should be designed together. Provider-specific API setup belongs alongside your email API reference and setup guides, while the webhook handler is the feedback loop that tells your application what happened after sending.
How to set up an inbound email webhook
Inbound processing has one extra dependency: email must be routed to the provider before the provider can call your HTTP endpoint. That usually means either using a provider-managed receiving address or changing DNS MX records for a receiving domain or subdomain.
Use a subdomain to avoid disrupting existing mail
If example.com already receives employee mail through Google Workspace or Microsoft 365, do not casually replace its root MX records. Use a dedicated subdomain such as:
replies.example.com
inbound.example.com
support-mail.example.com
Then send messages with a reply address such as ticket-123@replies.example.com, or publish a special intake address such as documents@inbound.example.com.
For SendGrid Inbound Parse, the documented MX record example is:
parse.example.com. 3600 IN MX 3 mx.sendgrid.net.
You then configure parse.example.com and your destination URL in the Inbound Parse settings. SendGrid says its inbound parser can post content, attachments, and headers for messages received at the configured hostname. (twilio.com)
The target, priority, TTL, and exact record set are vendor-specific. Copy the records generated in your provider dashboard exactly; do not reuse the SendGrid example for another provider. For providers such as Resend, receiving can depend on MX priority, and its documentation warns that the required receiving record must be the lowest priority value for the receiving domain. (resend.com)
Treat inbound email as untrusted input
An inbound webhook is an internet-facing ingestion endpoint. The sender address, display name, subject, HTML body, plain-text body, headers, URLs, and file attachments are all untrusted until your system validates them.
Apply these controls:
- Verify the provider webhook signature before processing the message.
- Store the original data separately from sanitized text rendered in your admin interface.
- Render HTML email in a sandboxed environment or sanitize it before display.
- Virus-scan and size-limit attachments before making them available to users or downstream automation.
- Do not fetch arbitrary URLs found in email content from a privileged server process.
- Match replies to an opaque, high-entropy ticket or conversation token rather than trusting the visible
Fromaddress alone. - Log routing decisions without logging secrets, authentication headers, or full sensitive content unnecessarily.
If you use an AI classifier for inbound support triage, pass it a constrained task and never allow the model output to directly execute sensitive operations. For example, a model may suggest a category of billing, but a separate authorization layer should decide whether the message can change an invoice, reset an account, or reveal customer data.
Inbound payload format differs by vendor
Do not build a generic parser around assumptions from one provider. SendGrid Inbound Parse uses multipart/form-data and can include parsed message parts and files. Postmark posts inbound messages as JSON. Resend’s email.received webhook provides metadata, while its documentation says bodies, headers, and attachments are retrieved through its receiving APIs rather than included in the webhook payload. (twilio.com)
That difference changes how you design storage and queues. A metadata-only event can be acknowledged quickly, then a worker can retrieve the content by the received-email ID. A multipart request may require stricter body-size limits and streaming attachment handling at the edge.
Security requirements for every email webhook
Webhook security is not merely a secret URL. URLs leak through logs, configuration exports, browser history, incident tickets, and third-party tooling. Build verification into the handler.
Verify authenticity with the vendor’s documented scheme
Use the provider’s official SDK if available. It removes subtle errors around base64 decoding, key rotation, signature versions, constant-time comparisons, and timestamp validation.
For Svix-style signatures, the signed content is the message ID, timestamp, and raw body joined by periods. The signature is HMAC-SHA256-based, and the raw body must not be altered before verification. (docs.svix.com)
For Mailgun, concatenate timestamp + token with no separator, calculate HMAC-SHA256 using your signing key, and compare it to the supplied signature. Mailgun also recommends optionally caching tokens to reject repeats and checking timestamps with a tolerance that accounts for delivery delays. (documentation.mailgun.com)
Defend against replay
A valid signed request can still be replayed if an attacker obtains it. Signature verification proves who signed the request, not necessarily that this is the first time you received it.
Use all three layers:
- Check the provider timestamp when the vendor supports it.
- Deduplicate with the provider delivery ID or message ID.
- Make the downstream action idempotent as a final safety net.
Svix documents replay attacks explicitly and states that its libraries reject timestamped webhook attempts more than five minutes away from the current time by default. That window is an implementation behavior of the Svix libraries, not a universal setting for all email providers. (docs.svix.com)
Avoid IP allowlists as the only control
IP filtering can be an additional layer when a provider publishes stable ranges and your infrastructure supports it, but it is not a substitute for cryptographic verification. Cloud network ranges change, proxies complicate source addresses, and a request from an allowed network is not inherently a valid event for your account.
Use TLS, signature verification, strict HTTP method handling, payload size limits, rate limits appropriate to your provider’s volume, and a separate route with no browser session authentication. Review framework CSRF settings: machine-to-machine webhook routes typically need signature verification rather than browser-oriented CSRF tokens. (docs.svix.com)
Common email webhook failures and how to fix them
“The webhook never arrives”
Check the endpoint externally with a real HTTPS request. Then check the provider’s delivery logs or webhook dashboard, the configured event selection, and your DNS or domain status for inbound use cases.
For inbound SendGrid configuration, confirm that the hostname has the required MX record and that the hostname configured in the provider matches the DNS hostname. SendGrid also warns that its Inbound Parse webhook does not follow redirects, so point it directly to the final HTTPS endpoint rather than a URL that returns 301 or 302. (twilio.com)
“Signature verification fails even though the secret is correct”
The usual cause is body transformation. Verify the original request body, not a JSON object serialized again by your framework. Also verify that you copied the webhook secret for the exact endpoint and environment; staging and production endpoints should normally have different secrets.
Check header casing through your hosting platform’s request API, but do not manually normalize signature values. For Svix-compatible integrations, pass the svix-id, svix-timestamp, and svix-signature values exactly as received to the official verifier. (resend.com)
“We processed the same bounce more than once”
Assume duplicates will occur. Add a unique database constraint keyed by the provider’s delivery ID, insert before side effects, and return success for a known duplicate. Do not treat duplicate delivery as evidence of a provider bug; at-least-once delivery is a normal reliability pattern. (resend.com)
“We return 200, but some records still go missing”
Your application is acknowledging too early. Do not return success after merely receiving the HTTP request if the event has not been stored durably. Persist the raw event and its deduplication key in the same transaction, then return 2xx; do downstream processing after that.
This design lets you replay internally if your CRM, database worker, or analytics system fails. It also gives support staff a factual record of what the provider sent.
“Inbound email broke our normal company mailbox”
You likely modified root-domain MX records rather than isolating receiving on a subdomain. Move the automation to a dedicated receiving subdomain and restore the mailbox provider’s MX records for the root domain. Email MX priorities and coexistence rules are provider-specific, so validate the exact DNS plan before publishing changes. (resend.com)
Observability: how to know your email webhook works
A green check in a provider dashboard is not enough. It usually confirms the provider could deliver an HTTP request, not that your business workflow completed correctly.
Track these measurements:
- Received events: count by provider, event type, endpoint, and environment.
- Verification failures: signature, timestamp, malformed-body, and missing-header failures.
- Acknowledgement latency: time from request arrival to
2xxresponse. - Duplicate rate: useful for understanding retries and replays.
- Queue lag and worker failures: proves the accepted event reached the business workflow.
- Unprocessed-event age: alerts when stored events are not being consumed.
- Domain and MX status: especially for inbound email receiving.
Create one end-to-end test for each critical event. For example: send a password-reset email to a controlled test address, confirm the provider event is received, confirm the database record is written exactly once, and confirm the event appears on the user’s message timeline. For inbound support, send a test message with a benign attachment, confirm the webhook is verified, confirm the attachment enters the scanning pipeline, and confirm a ticket is created without exposing unsanitized HTML.
Keep a replay tool or an internal “reprocess event” button. A raw-event archive plus idempotent workers means a temporary CRM outage does not force you to wait for provider retries or lose the ability to repair downstream state.
Email webhook provider differences that matter
The high-level pattern is portable, but do not assume payload shapes, retry rules, or security schemes transfer across vendors.
| Provider capability | Example implementation detail | Design implication |
|---|---|---|
| Resend | Svix-style headers and raw-body verification; at-least-once delivery | Deduplicate with svix-id; preserve raw text until verification. |
| Mailgun | HMAC signature uses body timestamp and token; documented retry behavior differs by status code | Validate payload signature and carefully choose non-200 responses. |
| Twilio SendGrid Event Webhook | Optional signed webhook with public-key verification; event tracking configuration | Separate outbound events from inbound parsing concerns. |
| Twilio SendGrid Inbound Parse | Receives mail for an MX-configured hostname and posts multipart data | Plan for attachment handling and direct, non-redirecting endpoint URLs. |
| Postmark Inbound | Posts parsed inbound email as JSON | Use a JSON-focused parser, but still treat HTML and attachments as untrusted. |
Choose an email provider based on your sending requirements first—deliverability controls, regions, APIs, pricing, message streams, and support needs—then verify that its webhook model fits your architecture. A provider with strong sending features but limited event retention may make an independent webhook event store more important.
Conclusion
The best email webhook implementation is deliberately boring: a dedicated HTTPS route, official signature verification against the raw payload, a durable event ledger, a unique delivery ID, a quick 2xx, and asynchronous idempotent processing. That foundation handles the realities of retries, replays, provider differences, and downstream outages.
Start with bounce, complaint, delivery, and unsubscribe events that protect your sending program. Add inbound receiving only after isolating DNS on a dedicated subdomain and designing for hostile content. Then instrument the full path so you can prove an event was received, verified, saved, processed, and safely replayed.
FAQ
What is an email webhook?
An email webhook is an HTTPS callback from an email provider to your application when an event occurs, such as a delivery, bounce, complaint, unsubscribe, click, or inbound message. It avoids repeatedly polling an API for changes. (twilio.com)
Do I need DNS records for an email webhook?
Not for outbound event webhooks: you generally only configure a public HTTPS endpoint in your provider. For inbound email webhooks, you usually need a provider-managed receiving address or MX records that route a domain or subdomain to the provider. (twilio.com)
How do I secure an email webhook?
Use HTTPS, verify the provider’s cryptographic signature against the raw request body, validate timestamps where supported, deduplicate delivery IDs, and make downstream processing idempotent. Do not rely only on a secret URL or IP allowlisting. (resend.com)
Why does my email webhook receive duplicate events?
Webhook delivery is commonly at least once. If your endpoint processes a request but the provider does not receive your acknowledgement, the provider can retry. Store a provider delivery ID with a unique constraint and return success for duplicates after verifying them. (resend.com)
Should a webhook handler update my CRM directly?
Usually, no. First verify and durably store the event, then enqueue CRM updates and other slow work. This lets you acknowledge the provider quickly and retry downstream failures without losing the original event.