Email to webhook: incoming email as JSON

Email to webhook means that every message sent to an address on your domain turns into an HTTP request to your application. With api.email you add one MX record and one route; from then on, each incoming email is parsed and POSTed to your URL as signed JSON within seconds. Your code never touches SMTP, IMAP or raw MIME, and it handles email exactly like any other event.

What “email to webhook” means

Email was designed for people reading mailboxes, not for programs. When an application needs to react to email, such as a customer replying to a ticket, a supplier sending an invoice or a system sending an alert, the traditional options are running a mail server or logging into a mailbox and polling it. Both mean dealing with the hardest parts of email yourself: MIME multipart structures, character encodings, attachments, quoted replies and sender authentication.

An email to webhook service moves all of that out of your code. The service receives the message, takes it apart and sends you a clean JSON document. You keep the part you care about: what to do with the message.

How it works

  1. MX record. You point a domain or subdomain, for example in.acme.com, at mx.api.email. Mail servers on the internet now deliver messages for that domain to api.email.
  2. Route. You create a route that matches an address such as [email protected], or every address with *@in.acme.com, and tells api.email where to forward it.
  3. Parse. For each message, api.email checks SPF, DKIM and DMARC, decodes the MIME parts, separates text, HTML and attachments and strips the quoted thread from replies.
  4. Deliver. api.email sends an email.received event to your URL, signed with the route’s secret. Your app answers with a 2xx status.
# DNS
in.acme.com.   MX   10   mx.api.email.

# Route
curl https://api.email/v1/routes \
  -H "Authorization: Bearer $API_EMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"match": "*@in.acme.com", "forward": "https://acme.com/hooks/inbound"}'
# → {"id": "rt_3k9d", "secret": "whsec_…"}

What your webhook receives

{
  "type": "email.received",
  "id": "in_5d1e7a",
  "from": "[email protected]",
  "to": "[email protected]",
  "subject": "Re: Your order #1042 shipped",
  "text": "Can I change the address?\n\nOn Mon, Acme wrote:\n> Your order has shipped…",
  "html": "<p>Can I change the address?</p>…",
  "reply_text": "Can I change the address?",
  "in_reply_to": "em_8f2c1a",
  "headers": { "message-id": "<…@example.com>" },
  "attachments": [],
  "auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass" },
  "received_at": "2026-10-25T09:14:03Z"
}

reply_text contains only what the person wrote, without the quoted history or the signature, which is what a ticketing system or a comment thread wants to store. in_reply_to links the reply to the email you sent through api.email, so you can attach it to the right conversation without parsing subjects. Attachments are listed with a download URL rather than embedded, which keeps webhook requests small.

Verify the signature

Anyone can send a POST request to a public URL, so check that each one came from api.email. When you create a route, the response includes its signing secret (whsec_…). Every delivery carries an api-email-signature header of the form t=<unix time>,v1=<hex signature>, where the signature is an HMAC-SHA256 of t + "." + raw body keyed with that secret. After a secret rotation the header briefly carries two v1 values; accept the request if either matches.

import crypto from 'node:crypto';

// Node.js — header: "t=1761383643,v1=<hex>"; after a secret rotation it carries two v1 values for 24 hours.
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = String(header ?? '').split(',').map((p) => p.trim().split('='));
  const t = parts.find(([k]) => k === 't')?.[1];
  const sigs = parts.filter(([k, v]) => k === 'v1' && /^[0-9a-f]{64}$/.test(v ?? '')).map(([, v]) => v);
  if (!/^\d+$/.test(t ?? '') || sigs.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
  return sigs.some((s) => crypto.timingSafeEqual(Buffer.from(s, 'hex'), expected));
}
import hashlib, hmac, re, time

def verify(raw: bytes, header: str | None, secret: str, tolerance: int = 300) -> bool:
    # Python — header: "t=1761383643,v1=<hex>"; after a secret rotation it carries two v1 values for 24 hours.
    t, sigs = None, []
    for part in (header or "").split(","):
        key, _, value = part.strip().partition("=")
        if key == "t" and value.isdigit():
            t = int(value)
        elif key == "v1" and re.fullmatch(r"[0-9a-f]{64}", value):
            sigs.append(value)
    if t is None or not sigs or abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in sigs)

Always verify against the raw bytes of the request, before any JSON parsing, and reject timestamps older than a few minutes so a captured request can’t be replayed.

Retries and replay

Your endpoint will be down at some point, for a deploy or an incident. Deliveries that fail or time out are retried with exponential backoff for up to 24 hours, and every attempt is logged with the status code your app returned. When you fix a bug in your handler, replay the affected messages from the dashboard instead of asking people to send them again. Because a message can arrive more than once, make processing idempotent by storing the event id and skipping ids you have already handled.

Common uses

  • Reply by email. Let users answer notifications, comments or tickets from their inbox, and post the reply in your app.
  • Support inbox. Turn support@ into structured tickets with sender, body and attachments.
  • Document intake. Receive invoices, receipts or signed forms by email and send the attachments into your processing pipeline.
  • Alerts from other systems. Many devices and legacy tools can only send email; forward their messages into chat or an incident tool.
  • End-to-end tests. Assert in CI that signup emails, magic links and one-time codes really arrive.

Email to webhook vs polling IMAP

Polling a mailbox over IMAP works for a script on a laptop, but it scales poorly for products. You need credentials for every mailbox, a connection that stays open or a schedule that adds delay, and your own MIME parsing and reply stripping. A webhook arrives seconds after the message, carries a parsed body, and needs nothing in your code except an HTTP handler. It also fits the rest of your stack: the same queue, logging and retry logic you already use for other webhooks.

For complete handlers with threading and attachments, see the guides for Node.js and Python, or read about the inbound email API.

FAQ

What is an email to webhook service?

It receives email for an address or domain, parses each message and sends it to a URL you choose as an HTTP POST, usually with a JSON body. Your app reacts to email the same way it reacts to any other webhook.

Is email to webhook better than polling IMAP?

For applications, yes. Webhooks arrive within seconds of the message, need no mailbox credentials in your code and come already parsed. IMAP polling adds delay, needs a connection per mailbox and leaves MIME parsing to you.

What happens if my webhook URL is down?

Failed or slow deliveries are retried with exponential backoff for up to 24 hours. Every attempt is logged, and you can replay any message from the dashboard once your endpoint is fixed.

How do I know a webhook really came from api.email?

Each request has an api-email-signature header with a timestamp and an HMAC-SHA256 signature of the raw body, keyed with the secret returned when you created the route. Recompute it, compare in constant time and reject old timestamps.

Get API key

Create your api.email account

What do you need?

Early access: accounts open in batches. By continuing you agree to the Terms and Privacy Policy.