Skip to content

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):

IngressWhat it isAvailability
HTTPS edgeA provider POSTs to <ingest-domain>/<slug>. Acked + durably buffered at the edge in <100 ms (never dropped); verified in the processing plane.Available
Queue ingestionYou 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 APIA 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, Stripe Stripe-Signature (300 s tolerance), Slack v0= (5-min replay window), Meta X-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 a 401 at the edge, never silently dropped).
  • The synchronous handshake the provider expects — Meta/WhatsApp hub.challenge echo, Slack url_verification, or a plain 200, 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):

bash
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:

bash
# 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 tee

What the mirror forwards

Part of the requestMirrored?
Method (GET/POST/PUT/PATCH/DELETE/HEAD)✅ exactly as received
Bodybyte-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-Depthadded (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's orders@ 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.

bash
# 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 of PASS · FAIL · GRAY · PROCESSING_FAILED (we trust the front door's crypto; we never re-run it), plus the sender's published dmarc_policy. auth is the collapsed SPF/DKIM/DMARC summary (verified / failed).
  • Dispositionstatus is accepted (parsed, stored, routed) or quarantined (a failing verdict: stored and reviewable, never routed). Nothing is silently dropped.
  • Parsed fieldsfrom, to, subject, text, html, the full headers map, size_bytes, the email's own date, and received_at (the ingestion time — distinct from date).
  • Attachmentsfilename + 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:

bash
# 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-0

A message looks like:

json
{
  "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

Next

Emithook · a Finnoto product