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 your endpoint's URL (<ingest-domain>/<org-slug>/<any/path>). 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, and its URL is its identity. On the shared ingest domain the URL starts with your org slug, followed by any path you like: https://<ingest-domain>/<org-slug>/shopify/orders. On your own domain, any path: https://hooks.acme.com/shopify/orders. Matching is exact: letter case and a trailing slash don't matter, but …/orders and …/orders/v2 are two different endpoints, and a request to a URL nobody registered isn't delivered. It is still accepted and recorded in the request log.

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
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'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: failed only when one of them is FAIL or PROCESSING_FAILED, otherwise verified (GRAY — no signature, or no DMARC policy — is not a failure).
  • Disposition — status is accepted (parsed, stored, routed) or quarantined (a failing verdict: stored and reviewable, never routed). Nothing is silently dropped.
  • Parsed fields — from, to and cc (lists of plain addresses), subject, text, html, the full headers map (each header's original text), size_bytes, the email's own date, and received_at (the ingestion time — distinct from date).
  • Attachments — id (att_…, the download key), name (the exact Unicode filename), size (bytes), content_type, content_id (an inline image is referenced in the HTML as cid:<content_id>) and disposition (inline / attachment); 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. Address both by their opaque, URL-safe ids — the alias id (al_…, from GET /v1/aliases) and the message id (evt_…, on every message). The alias address and the RFC Message-ID also work, percent-encoded.

bash
# List received mail for an alias (newest first, cursor-paginated)
curl -s https://api.emithook.com/v1/aliases/al_01KXBRV8HB64MA2H6QM49B2V8F/messages \
  -H "Authorization: Bearer $EMITHOOK_API_KEY"

# One message — parsed fields + per-field verdicts
curl -s https://api.emithook.com/v1/aliases/al_01KXBRV8HB64MA2H6QM49B2V8F/messages/evt_01M50GHMJRP15Z4602V9XFVKDA \
  -H "Authorization: Bearer $EMITHOOK_API_KEY"

# Download the original .eml, and an attachment by its id (`attachments[].id` on the message)
curl -s https://api.emithook.com/v1/aliases/al_01KXBRV8HB64MA2H6QM49B2V8F/messages/evt_01M50GHMJRP15Z4602V9XFVKDA/raw \
  -H "Authorization: Bearer $EMITHOOK_API_KEY" -o message.eml
curl -s https://api.emithook.com/v1/aliases/al_01KXBRV8HB64MA2H6QM49B2V8F/messages/evt_01M50GHMJRP15Z4602V9XFVKDA/attachments/att_01M50GHMJRP15Z4602V9XFVKDA_0 \
  -H "Authorization: Bearer $EMITHOOK_API_KEY" -o attachment-0

Routed to a destination, an email arrives as an email.received notice rather than the raw MIME — its id, alias_id, from/to/cc/subject, attachment list and these exact URLs under api — so the consumer fetches attachments and the raw message with its own key. A queue destination (SQS, …) gets the notice alone; an HTTPS destination gets the same notice plus the parsed text, html and headers. The full process, with a sample notice and consumers, is in Receive email into SQS.

A message looks like:

json
{
  "id": "evt_01M50GHMJRP15Z4602V9XFVKDA",
  "message_id": "<abc@buyer.com>",
  "event_id": "evt_01M50GHMJRP15Z4602V9XFVKDA",
  "alias": "support@acme.com",
  "from": "jane@buyer.com",
  "to": ["support@acme.com"],
  "cc": ["ops@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": [
    { "id": "att_01M50GHMJRP15Z4602V9XFVKDA_0", "name": "receipt.pdf", "size": 18234, "content_type": "application/pdf",
      "content_id": null, "disposition": "attachment" }
  ]
}

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. To re-deliver every email an alias received in a time range, use POST /v1/aliases/{alias}/replay — see Receive email into SQS → Re-deliver.

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.
  • Receive email into SQS — an address routed to a queue: create it in one request, consume email.received notices, fetch the message and attachments.

Next ​

Emithook · a Finnoto product