Email to webhook: the complete developer's guide
Turn inbound email into webhook calls your code can consume: payload design, retries, idempotency, and verifying HMAC signatures, with working snippets.
Why email → webhook is such a useful primitive
A surprising amount of the world's data has exactly one delivery mechanism: email. Payment notifications from providers with no API, alerts from legacy systems, order confirmations, form submissions, bounce reports. If you can turn "an email arrived" into "an HTTP POST hit my endpoint with structured JSON", all of it becomes programmable.
The good news is that the receiving side is just a small HTTP handler. The parts people get wrong are the contract around it: what the payload should look like, what happens when your endpoint is down, and how you stop attackers from POSTing forged "emails" at you. This guide covers all three.
Designing the payload
A good email webhook payload gives you the decoded, structured essence of the message, never raw MIME. You want stable identifiers for deduplication, separated text and HTML bodies, and attachment metadata with the content itself fetched separately or size-capped. Here's the shape a well-behaved provider delivers:
- A unique id: your idempotency key for deduplication
- The receiving address: one endpoint can serve many pipelines
- Decoded text and html bodies, never quoted-printable soup
- Attachment metadata up front; large binaries fetched on demand
{
"event": "email.received",
"id": "em_9f2c81d4a0",
"received_at": "2026-03-28T09:41:07Z",
"address": "orders@yournick.hidemy.world",
"from": { "name": "Acme Store", "email": "noreply@acme.example" },
"subject": "Your order #48211 has shipped",
"headers": {
"message-id": "<20260328094105.1a2b@acme.example>",
"list-id": null
},
"text": "Good news! Order #48211 shipped via DHL...",
"html": "<html>...</html>",
"attachments": [
{ "filename": "invoice.pdf", "content_type": "application/pdf", "size": 48210 }
]
}Delivery semantics: assume everything fails
Your endpoint will go down: a deploy, a crash, a cloud wobble. A serious sender retries failed deliveries with exponential backoff over hours, which means your handler must be idempotent: the same email may arrive twice. Store the payload id and skip duplicates before doing any side effects.
Respond fast, too. Return 200 as soon as the payload is safely persisted or queued, and do the actual work (parsing PDFs, calling your CRM, posting to Slack) asynchronously. A handler that does heavy lifting inline will hit the sender's timeout, get retried, and hand you the duplicate problem with interest.
Think about ordering as well. Retries mean deliveries can arrive out of sequence: the "order shipped" email may reach your handler before the delayed "order confirmed" one. If sequence matters to your logic, order by the message's own timestamps or identifiers when processing, never by arrival time at your endpoint.
Verifying signatures: don't trust naked POSTs
Anyone who discovers your endpoint URL can POST fabricated "emails" at it. The standard defence is an HMAC signature: the provider signs the raw request body with a shared secret and sends the result in a header; you recompute it and compare. Two rules matter: compute the HMAC over the raw bytes (before any JSON parsing, since re-serialised JSON won't match), and compare with a constant-time function to avoid timing attacks.
Here's the whole thing in Node:
import crypto from "node:crypto";
import express from "express";
const app = express();
// Capture the RAW body: signatures are computed over bytes, not parsed JSON
app.post("/hooks/email", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.get("X-Signature-SHA256") ?? "";
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(req.body) // Buffer of raw bytes
.digest("hex");
const ok =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) return res.status(401).end();
const email = JSON.parse(req.body);
// enqueue for processing, then acknowledge quickly
res.status(200).end();
});Building the receiving endpoint: a checklist
That's the entire discipline. None of it is exotic: it's the same hygiene as any webhook consumer, applied to a data source that happens to originate as email. If you already consume payment or repository webhooks, your existing middleware for signatures, deduplication and queueing slots straight in; only the payload shape is new.
- Verify the HMAC signature on the raw body before anything else
- Deduplicate on the payload id: retries will happen
- Persist or enqueue first, process asynchronously, respond 200 fast
- Log rejected requests: repeated signature failures mean probing
- Treat body content as untrusted input: sanitise before rendering anywhere
- Use one secret per pipeline so a leak has a small blast radius
Wiring it up end to end
The missing piece is the sender side: something that receives the actual email, filters out the noise, and makes the POST. You can self-host this (an SMTP server, a MIME parser, a retry queue: a weekend that becomes a quarter), or use a hosted pipeline. On HideMy.world the whole chain is configuration: create an address such as alerts@yournick.hidemy.world, attach rules so only the mail you care about gets through, optionally add a transformer to reshape the JSON to your exact schema, and set a webhook channel as the destination. Signed delivery and retries are handled for you; your side is just the handler above.
Test with real traffic before trusting it: point a sender at the address, watch the payloads arrive, and deliberately return 500 once to confirm the retry behaviour matches your expectations. An hour of poking saves a production incident.
Where to take it next
Once inbound email is an HTTP event, it composes with everything else you run: queue it, fan it out, feed it to a workflow engine or an LLM. The pattern scales down as well as up: a ten-line handler forwarding shipping updates to a Telegram group is just as legitimate as an invoice-ingestion service.
A good first project is instrumenting something you already receive: point a monitoring or billing sender at a fresh address, stand up the twenty-line verified handler, and watch a previously opaque email stream become queryable events in your own database. Email spent decades as the place where structured data went to die; a webhook is how you get it back out.