Receive webhooks
The relay side of Emithook: give a provider a stable place to send to, and Emithook acknowledges instantly, verifies authenticity, and routes the event to your systems — losing nothing if you're down. This page is the map; follow the links for detail.
How an event arrives
A provider can reach you four ways. All of them mint a time-sortable event id and enter the same pipeline (ack & durably buffer → verify → route → audit → deliver):
| Ingress | What it is | Availability |
|---|---|---|
| HTTPS edge | A provider POSTs to <ingest-domain>/<slug>. Acked + durably buffered at the edge in <100 ms (never dropped); verified in the processing plane. | Available |
| Queue ingestion | You produce to your own broker (SQS/SNS, Pub/Sub, Kafka, AMQP, NATS, Redis…) and Emithook consumes it. Removes per-request cost for high-volume senders. | Available |
| Inbound email (MX) | Mail to <alias>@in.<ingest-domain> is parsed (headers, bodies, attachments) into an event after SPF/DKIM/DMARC checks. | Available |
| Send API | A direct API call — see Send. | Available |
Webhooks and email share one dedicated ingest domain (<ingest-domain>), kept separate from the marketing/console site for a hard cookie boundary off the auth domain plus reputation isolation. <ingest-domain> is a placeholder — the real name is an operator choice and config-driven (the edge is domain-agnostic). See the glossary for precise definitions.
Endpoints & provider presets
An inbound endpoint is the entry you hand to a provider. Picking a provider preset (Shopify, Stripe, Slack, Meta, Razorpay, generic…) configures two things automatically:
- Signature verification — e.g. Shopify
X-Shopify-Hmac-Sha256, StripeStripe-Signature(300 s tolerance), Slackv0=(5-min replay window), MetaX-Hub-Signature-256. This runs in the processing plane, not at the edge (the edge accepts and buffers first): a verified event is routed, while a bad signature is quarantined — durable and inspectable, but never delivered (not a401at the edge, never silently dropped). - The synchronous handshake the provider expects — Meta/WhatsApp
hub.challengeecho, Slackurl_verification, or a plain200, answered at the edge.
Zero DNS: a new endpoint is live immediately on the shared <ingest-domain>. A custom domain is optional branding — for webhooks you CNAME your subdomain to our ingest host (TLS auto-provisioned via Cloudflare for SaaS); for email you publish MX + SPF/DKIM/DMARC records (delivered via SES).
Routing, filters & transformations
Each endpoint carries one or more routes that fan an incoming event out to one or more destinations. A route can:
- Filter — header/path/JSON-path match to drop or conditionally route events. Available
- Transform — reshape/enrich/redact the payload with sandboxed JS, versioned and testable against a saved sample. Available
Unroutable events are never dropped — they're parked and become replayable once a route exists.
Mirror an endpoint
A mirror forwards a byte-exact copy of every request an endpoint accepts to an HTTPS URL you control — same method, raw body, and query, with all headers except hop-by-hop — so the receiver can verify the provider's original signature over the exact bytes it saw. A mirror is a reusable destination (type: mirror): create it once, attach it to as many endpoints as you like, and it gets the same reachability check, retries, circuit breaker and rollups as any destination.
Unlike a delivery destination, a mirror is not routed — no filter or transform ever touches it. You attach it to an endpoint's dedicated mirror slot (in the console: the endpoint's ⋯ menu → Mirror to a URL), and it fires for every accepted request, in parallel with the rest of the pipeline and pre-verification (a signature-failed request still mirrors; the downstream does its own verification). See the request pipeline for exactly where it branches.
Create the mirror destination (check-then-create on its URL):
curl -X POST https://api.emithook.com/v1/destinations \
-H "authorization: Bearer $EMITHOOK_API_KEY" \
-H "content-type: application/json" \
-d '{"name":"orders-backup","type":"mirror","url":"https://backup.acme.example/hooks"}'
# → 201 { "id": "dst_…", "type": "mirror", "validation": "valid", … }Then attach it to an endpoint's mirror slot in the console. From then on, every request the endpoint accepts also arrives at https://backup.acme.example/hooks byte-for-byte — so this passes at your receiver:
# at your mirror URL: the provider's own HMAC still verifies over the raw body we forwarded
computed = HMAC_SHA256(secret, raw_request_body)
assert computed == request.headers["X-Provider-Signature"] # ✅ survives the teeWhat the mirror forwards
| Part of the request | Mirrored? |
|---|---|
Method (GET/POST/PUT/PATCH/DELETE/HEAD) | ✅ exactly as received |
| Body | ✅ byte-exact — never parsed or re-serialized |
| Query string | ✅ appended to the mirror URL |
Headers (provider signature, Content-Type, etc.) | ✅ forwarded verbatim |
Hop-by-hop headers (Connection, Transfer-Encoding, Keep-Alive, Upgrade, TE, Trailer) | ❌ stripped |
Host / Content-Length | ❌ set by the target / recomputed |
Emithook-Mirror-Id, Emithook-Mirror-Depth | ➕ added (the request id + anti-loop hop depth) |
The mirror outcome never affects the synchronous response your provider gets — that was already answered at the edge. To prevent loops, a mirror URL may not point at an Emithook ingest domain, and a request that arrives already bearing Emithook-Mirror-Depth ≥ 1 is not re-mirrored.
Inbound email & the inbox
Mail sent to an alias is received by the MX engine, which runs SPF, DKIM, DMARC, spam and virus checks, parses the message (headers, text/HTML bodies, attachments), and turns it into an event on the same pipeline as a webhook — so inbound email routes, transforms, retries and replays exactly like an HTTP event. An email's event id is its delivery event id: the inbox row and the delivery record are one and the same.
Defining an inbound-email address
Before mail can arrive you create an alias — the inbound-email address itself. It comes in two shapes:
- Platform —
<label>.<org-slug>@<platform-domain>(e.g.orders.acme@emhk.in). You choose a single lowercase<label>([a-z0-9-], no dots); the.<org-slug>@<platform-domain>suffix is fixed and server-supplied, so the one shared MX namespaces every tenant'sorders@apart — exactly like a platform webhook URL carries your org slug. No DNS; live immediately. - Custom domain —
<local>@<host>(e.g.support@mail.acme.com) on one of your MX-verified email domains.<local>is any email local-part, or exactly*for a catch-all (catch-all is custom-domain only). Add and verify the email domain first.
The address is computed and validated server-side from kind, localPart and (for custom) domainId — you never send the full address, and you can't forge another org's. Which destinations the mail fans out to, an optional sender allow-list, and a max message size live on the alias and stay editable; the address and kind are immutable.
# Platform alias → orders.<org-slug>@<platform-domain>
curl -s https://api.emithook.com/v1/aliases \
-H "Authorization: Bearer $EMITHOOK_API_KEY" -H "Content-Type: application/json" \
-d '{"kind":"platform","localPart":"orders","destinationIds":["dst_…"]}'
# Custom-domain alias → support@mail.acme.com (on a verified email domain)
curl -s https://api.emithook.com/v1/aliases \
-H "Authorization: Bearer $EMITHOOK_API_KEY" -H "Content-Type: application/json" \
-d '{"kind":"custom","domainId":"dom_…","localPart":"support"}'
# List aliases, then update one alias's routing destinations
curl -s https://api.emithook.com/v1/aliases -H "Authorization: Bearer $EMITHOOK_API_KEY"
curl -s -X PATCH https://api.emithook.com/v1/aliases/al_… \
-H "Authorization: Bearer $EMITHOOK_API_KEY" -H "Content-Type: application/json" \
-d '{"destinationIds":["dst_a","dst_b"]}'In the console the same lives on the address detail page: define the address once, then attach its routing destinations there.
Reading received mail
Each received message carries:
- Verdicts — per-field
spf/dkim/dmarc/spam/virus, each one ofPASS · FAIL · GRAY · PROCESSING_FAILED(we trust the front door's crypto; we never re-run it), plus the sender's publisheddmarc_policy.authis the collapsed SPF/DKIM/DMARC summary (verified/failed). - Disposition —
statusisaccepted(parsed, stored, routed) orquarantined(a failing verdict: stored and reviewable, never routed). Nothing is silently dropped. - Parsed fields —
from,to,subject,text,html, the fullheadersmap,size_bytes, the email's owndate, andreceived_at(the ingestion time — distinct fromdate). - Attachments —
filename+content_type+size_bytes; the bytes (and the raw.eml) stay in object storage and stream on demand.
List an alias's mail, open one message, and download its raw MIME + an attachment over the headless /v1 API:
# List received mail for an alias (newest first, cursor-paginated)
curl -s https://api.emithook.com/v1/aliases/support/messages \
-H "Authorization: Bearer $EMITHOOK_API_KEY"
# One message — parsed fields + per-field verdicts
curl -s "https://api.emithook.com/v1/aliases/support/messages/<message-id>" \
-H "Authorization: Bearer $EMITHOOK_API_KEY"
# Download the original .eml, and the first attachment
curl -s "https://api.emithook.com/v1/aliases/support/messages/<message-id>/raw" \
-H "Authorization: Bearer $EMITHOOK_API_KEY" -o message.eml
curl -s "https://api.emithook.com/v1/aliases/support/messages/<message-id>/attachments/0" \
-H "Authorization: Bearer $EMITHOOK_API_KEY" -o attachment-0A message looks like:
{
"message_id": "<abc@buyer.com>",
"event_id": "evt_01JX9…",
"alias": "support",
"from": "jane@buyer.com",
"to": ["support@acme.com"],
"subject": "Order #9001 — when does it ship?",
"received_at": "2026-06-21T09:00:05.000Z",
"date": "2026-06-21T08:59:40.000Z",
"status": "accepted",
"auth": "verified",
"verdicts": { "spf": "PASS", "dkim": "PASS", "dmarc": "PASS", "spam": "PASS", "virus": "PASS" },
"endpoint_id": "ep_01JX9…",
"attachments": [{ "filename": "receipt.pdf", "content_type": "application/pdf", "size_bytes": 18234 }]
}The same data drives the console inbox: a list with SPF·DKIM·DMARC chips, and a two-pane message view (Rendered / Plain / Raw / Headers / Files tabs) whose Payload tab links to the delivery attempts. To re-run routing for a message, Re-deliver replays its event_id — the same POST /v1/events/{id}/replay a webhook uses.
Reliability you get for free
Once an event is in, delivery is signed, retried with backoff + jitter, circuit-broken per destination, and dead-lettered (replayable) on final failure. The full contract is in API conventions → delivery semantics.
Tutorials
- Receive a Shopify webhook — a complete inbound walkthrough: verify, fan out to an API and a queue, replay.
Next
- Send webhooks — the outbound half of the engine.
- Destinations & validation — where events go.
- API reference · CLI · MCP server