Email webhooks: how to receive email as JSON
An email webhook POSTs inbound mail to your endpoint as JSON. Learn what an email webhook is, how to receive one, verify signatures, and pick a provider.
Dvir Atias
Founder
An email webhook is an HTTP callback that delivers an incoming email to your application as structured data. When a message lands in an inbox, the provider parses the raw MIME, then sends an HTTP POST whose JSON body carries the sender, subject, text, HTML, and attachments to a URL you registered in advance. Your code reacts the moment mail arrives instead of polling a mailbox over IMAP.
This is the pattern behind support bots that reply to email, agents that read receipts, and pipelines that file invoices on their own. Here is what an email webhook actually is, the two different things people mean by the term, how to build a receiving endpoint, and how the major providers compare.
What is an email webhook?
Traditional email access is pull-based. You connect to an IMAP server, list folders, fetch messages, and diff against what you saw last time. It is stateful, slow, and awkward for software that just wants to know "did a new email arrive, and what did it say."
An email webhook inverts that. You give the provider a URL once. From then on, every qualifying message is decoded from its raw form into clean JSON and pushed to your endpoint as an HTTP POST. There is no connection to hold open, no IDLE loop, no cursor to track. If your handler returns a 2xx status, the delivery is done. If it fails, the provider retries.
The parsing step matters. Raw email is MIME: nested multipart bodies, quoted-printable and base64 encodings, header folding, and attachments encoded inline. A good email webhook provider does that decoding for you and hands over a flat object with from, to, subject, text, html, and a list of attachments, so you never touch a MIME parser.
Inbound webhooks vs event webhooks
"Email webhook" gets used for two related but distinct things, and mixing them up causes most of the confusion.
- Inbound email webhooks fire when mail arrives at an address you own. This is the email-to-webhook direction: someone emails
intake@yourdomain.comand you get a JSON POST with the parsed message. This is what you want for reply bots, ticket intake, and parsing incoming documents. - Event webhooks fire about mail you sent. These report delivery lifecycle events - delivered, bounced, opened, clicked, complained - so you can update a database or dashboard. They carry an event, not a full inbound message.
Most providers offer both. When someone asks "how do I forward email to a webhook," they mean the inbound direction. When they ask about "sent email webhooks," they mean events. AgenticEmail exposes both through one registration and one signed payload format, which keeps your handler simple.
How do you receive email as a webhook?
Building a receiving endpoint takes three steps: stand up an HTTPS route, register it with your provider, and verify what shows up.
- Expose a public HTTPS URL. Webhooks are POSTed from the internet, so the endpoint must be reachable and TLS-terminated. In development, a tunnel like ngrok works.
- Register the URL with the event types you care about.
- Handle the POST, verify its signature, and return
200fast.
Register the endpoint with a single call:
curl -X POST https://api.agenticemail.dev/v1/webhooks \
-H "Authorization: Bearer $AGENTICEMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/email",
"event_types": ["message.received"]
}'A minimal Express handler that reads the parsed message looks like this:
import express from "express";
const app = express();
app.post("/hooks/email", express.json(), (req, res) => {
const event = req.body;
if (event.type === "message.received") {
const msg = event.message;
console.log(`New mail from ${msg.from}: ${msg.subject}`);
// hand msg.text to your agent, file the attachment, open a ticket...
}
res.sendStatus(200);
});
app.listen(3000);Return the 200 quickly and do slow work (calling an LLM, writing to a queue) after you respond, so a retry is never triggered by your own latency.
How do you verify an email webhook signature?
Anyone who learns your URL can POST to it, so you must confirm each request really came from your provider before trusting it. AgenticEmail signs deliveries using the Standard Webhooks spec, the same scheme a growing number of providers share. Three headers travel with every POST: webhook-id, webhook-timestamp, and webhook-signature. You verify by recomputing an HMAC-SHA256 over id.timestamp.rawBody with the signing secret shown once at creation time (it starts with whsec_).
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.AGENTICEMAIL_WEBHOOK_SECRET);
app.post("/hooks/email", express.raw({ type: "*/*" }), (req, res) => {
let event;
try {
event = wh.verify(req.body, {
"webhook-id": req.header("webhook-id"),
"webhook-timestamp": req.header("webhook-timestamp"),
"webhook-signature": req.header("webhook-signature"),
});
} catch {
return res.sendStatus(401);
}
// event is trusted from here
res.sendStatus(200);
});Two rules matter. Verify against the raw request body, not a re-serialized object, because JSON key ordering changes the bytes and breaks the HMAC. And reject deliveries whose timestamp is more than a few minutes old, which stops replay attacks.
Retries, idempotency, and delivery logs
Networks fail, so email webhooks retry. If your endpoint is down or returns a non-2xx status, the provider re-sends the same delivery on a backoff schedule. That means your handler can see the same message twice, so it must be idempotent: key your side effects on the message id (or the webhook-id header) and ignore a repeat you have already processed.
When you need to inspect or replay history, AgenticEmail keeps a delivery log. GET /v1/webhooks/{id}/deliveries returns each attempt with its response code, and you can trigger a redelivery from there - useful when you ship a bug fix and want to reprocess the mail you dropped.
Email webhook providers compared
Every major email platform can turn inbound mail into an HTTP POST. They differ in how you route addresses, whether payloads are signed by default, and whether the product is built for humans or for agents.
| Provider | Inbound feature | Signed payload | Best for |
|---|---|---|---|
| SendGrid | Inbound Parse | Optional | Existing SendGrid senders |
| Mailgun | Routes | Yes (HMAC) | Rule-based routing |
| Postmark | Inbound streams | Basic auth / token | Transactional email teams |
| CloudMailin | Inbound only | HMAC | Pure email-to-webhook |
| AgenticEmail | message.received | Yes (Standard Webhooks) | AI agents and per-inbox scopes |
For the two most common starting points, see the SendGrid Inbound Parse guide and the Mailgun webhook guide. The tradeoff is usually setup friction: the classic providers ask you to point MX records and wire up parse rules, while agent-first tools hand you a working address in one call.
A worked example with AgenticEmail
Here is the full inbound loop end to end. First, create an inbox:
curl -X POST https://api.agenticemail.dev/v1/inboxes \
-H "Authorization: Bearer $AGENTICEMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "username": "intake" }'That returns a live address, intake@inbox.agenticemail.dev. Register a webhook against it (reuse the curl from earlier, optionally scoping to this inbox with inbox_ids). Now email that address, and your endpoint receives a signed message.received payload:
{
"type": "message.received",
"inbox_id": "inbox_123",
"message": {
"id": "msg_456",
"from": "customer@example.com",
"to": "intake@inbox.agenticemail.dev",
"subject": "Invoice for July",
"text": "Hi, attaching this month's invoice.",
"received_at": "2026-07-01T12:00:00Z"
}
}From here your agent reads message.text, does its work, and replies over REST. If your agent has no public URL - a local script, a serverless job, a notebook - subscribe to the WebSocket stream at wss://api.agenticemail.dev/v1/events?token=am_... instead and get the same payloads pushed to a long-lived connection. The full walkthrough lives in Give your agent an inbox, and every field is documented in the docs.
Frequently asked questions
Can I forward email to a webhook?
Yes, that is exactly what an inbound email webhook does. You create an address, point a webhook at your endpoint, and every message sent to that address is parsed and POSTed to you as JSON. No IMAP, no polling, no mail server to run.
What is the difference between an inbound webhook and an event webhook?
An inbound webhook fires when someone emails an address you own and carries the full parsed message. An event webhook fires about mail you sent and reports lifecycle events like delivered, bounced, opened, or complained. AgenticEmail sends both through the same signed endpoint - you choose which event_types to subscribe to.
How do webhook retries work?
If your endpoint returns a non-2xx status or times out, the provider re-sends the same delivery on a backoff schedule. Because a message can arrive more than once, make your handler idempotent by deduplicating on the message id. You can inspect every attempt and redeliver from GET /v1/webhooks/{id}/deliveries.
Do I need a public server to receive email webhooks?
For webhooks, yes - the provider POSTs from the internet, so the URL must be reachable over HTTPS. If you cannot host a public endpoint, use the WebSocket event stream instead, which pushes the same payloads to an outbound connection your agent opens.
Which events can I subscribe to?
AgenticEmail emits message.received, message.sent, message.delivered, message.bounced, message.complained, message.opened, message.clicked, domain.verified, and reply.suggested. Subscribe to only the ones your app acts on to keep payload volume low.
How do I verify the payload is genuine?
Recompute the HMAC-SHA256 signature over the raw body using your whsec_ secret and compare it to the webhook-signature header, following the Standard Webhooks spec. Reject anything that fails or whose webhook-timestamp is stale.