--- url: /docs/guide/introduction.md --- # Introduction **Emithook** is open-source webhook infrastructure. It does two jobs on a single delivery engine: * **Receive & relay** — give a provider a stable URL (or let them write to a queue, or send you email), and Emithook **acknowledges in under 100 ms and durably buffers** the event — so your service can be down for hours and lose nothing. It then verifies the signature in the **processing plane** and routes the event to your systems. * **Send & broadcast** — push your own events out as webhooks-as-a-service. One API call delivers to a URL or a queue, or fans out to every endpoint an application's customers have subscribed. The same machinery — signing, retries, circuit-breaking, logs, archive, and one-click replay — is shared across both directions. ## Four ways an event enters | Ingress | Use it when | | --- | --- | | **HTTPS edge** | A provider POSTs to your `/` URL. | | **Queue ingestion** | You write to a broker (SQS/Pub/Sub/Kafka…) and Emithook consumes it. | | **Inbound email (MX)** | Mail to `@in.` is parsed into an event. | | **Send API** | You call `POST /v1/send` with a payload and a destination. | Webhooks and email share a dedicated, config-driven ingest domain (shown as ``), separate from the console/marketing site. ## Three delivery patterns 1. **Fan-out** — one event always goes to the same set of destinations. 2. **Per-tenant** — register customers as *applications*; `POST /v1/app/{tenant}/event` resolves *their* endpoints. 3. **Direct send** — ad-hoc `POST /v1/send` to a single URL or queue. ## Why teams use it * **Zero setup.** You're live immediately on our domain for both webhooks and email — bringing your own domain is optional. * **Reliability you'd otherwise build.** Backoff retries with jitter, dead-letter queue, circuit breaker, SSRF-guarded egress. * **Agent-native.** Console, CLI, the TypeScript SDK and an MCP server are all thin clients over one scoped API (Python/Go SDKs + a Terraform provider are on the [roadmap](/guide/roadmap)). * **Open-source, no lock-in.** Apache-2.0, self-hostable with pluggable adapters (Postgres, NATS, Redis, MinIO) — or use the managed cloud, with data resident in your region (India / `ap-south-1` today, multi-region by design). ## Where to next * [Quickstart](/guide/getting-started) — a delivered, signed webhook in a few minutes. * [Receive webhooks](/guide/receive) — the inbound relay: ingress, presets, routing. * [Send webhooks](/guide/send) — outbound webhooks-as-a-service. * [Glossary & data model](/guide/glossary) — the precise vocabulary every surface uses. * [Roadmap & availability](/guide/roadmap) — what ships when (read this before calling the API). --- --- url: /docs/reference/operations.md --- # API operations Every endpoint, grouped by area in the sidebar with method badges. Pick an operation to see its parameters, request and response schemas, and copy-paste code samples — all generated from our [OpenAPI spec](/docs/openapi.yaml), so they never drift. New to the API? Start with the [API reference](/reference/api) for authentication, status codes and the fundamentals, then explore the operations here. --- --- url: /docs/reference/api.md --- # API reference The Emithook API is **one scoped REST/JSON surface** — the substrate the console, [CLI](/reference/cli), SDKs and [MCP server](/reference/mcp) all run on. No surface has a private backdoor, so anything you can do in the console you can do over the API. This page is the on-ramp: authenticate, make a call, read the response, handle errors. For the exhaustive cross-cutting rules see [API conventions](/reference/conventions); to browse every endpoint, open [API operations](/reference/operations/). ::: warning Pre-release Emithook is `v0.1`; endpoints ship by [roadmap phase](/guide/roadmap). Paths not yet live return `501 not_implemented`. The management + Send API is targeted for Phase 2. ::: ## Base URL & versioning ``` https://api.emithook.com ``` Every path is versioned with a `/v1` prefix. Changes are **additive** within a version — new fields and optional parameters can appear at any time, so your client must **ignore unknown fields**. Breaking changes ship under a new prefix (`/v2`). All requests and responses are JSON. Send `Content-Type: application/json` on any request with a body. ## Authentication Every request carries a **scoped API key** as a bearer token: ```bash curl https://api.emithook.com/v1/events \ -H "Authorization: Bearer ek_live_xxxxxxxxxxxxxxxx" ``` Get a key from the console under **Settings → API Keys**. Keys are environment- and scope-bound: * **Environment** — `ek_live_…` (production) or `ek_test_…` (the test environment). A key only ever sees its own environment's data. * **Scope** — three levels, each a superset of the one below: | Scope | Grants | | --- | --- | | `read` | Query events, attempts, metrics, config, inbox. Never mutates. | | `write` | `read` + send, replay, redrive, create/update config, rotate secrets. | | `admin` | `write` + manage keys, members, domains, billing. | A call needing more scope than the key holds returns `403 insufficient_scope`. A missing or invalid key returns `401 unauthenticated`. Treat keys as secrets — keep them server-side, and rotate from the console (or [`emithook keys rotate`](/reference/cli#keys)). ## Make your first call Two calls — one read, one write — show the whole shape of the API. **List recent events** (read): ```bash curl https://api.emithook.com/v1/events?status=failed \ -H "Authorization: Bearer $EK_KEY" ``` ```json // → 200 OK { "data": [ { "id": "evt_01JX9…", "event_type": "orders/create", "endpoint": "/shopify-store/orders", "status": "failed" } ], "next_cursor": null } ``` **Send a webhook** (write): ```bash curl -X POST https://api.emithook.com/v1/send \ -H "Authorization: Bearer $EK_KEY" \ -H "Idempotency-Key: 7e3b9c1a-…" \ -H "Content-Type: application/json" \ -d '{ "destination": "dst_01JX9", "event_type": "invoice.created", "payload": { "id": "INV-2026-001", "amount": 4999 } }' ``` ```json // → 202 Accepted { "message_id": "msg_01JX9…" } ``` The [Quickstart](/guide/getting-started) walks the full create-destination → send → verify loop. ## Requests **Resource IDs** are prefixed and ULID-based, so they're globally unique, time-sortable, and self-describing (`evt_`, `msg_`, `dst_`, `ep_`, `app_`, `ek_`). Treat them as opaque except for the prefix — see the [ID table](/guide/glossary#id-prefixes). **Idempotency.** `POST` requests that send or create accept an `Idempotency-Key` header (a UUID you generate). Retries with the same key inside the window (~12–24 h) return the **original** response and create no second effect — make every write retry-safe. Details in [conventions → idempotency](/reference/conventions#idempotency). ## Responses & status codes Success uses the conventional 2xx codes: | Code | Meaning | | --- | --- | | `200 OK` | Read succeeded; body is the resource or a page. | | `201 Created` | Resource created (e.g. a destination, `pending` validation). | | `202 Accepted` | Work queued (send, replay, redrive) — `message_id` returned. | | `204 No Content` | Succeeded, no body (e.g. setting a secret). | Every non-2xx response shares **one error envelope** — always log `request_id`, which correlates end-to-end with the [delivery logs](/guide/glossary#logs-archive-replay): ```json { "error": { "type": "validation_failed", "message": "destination connectivity check failed: 403 from target", "request_id": "req_01JX9…", "details": { "check": "connectivity" } } } ``` | HTTP | `type` | When | What to do | | --- | --- | --- | --- | | `400` | `bad_request` | Malformed JSON or params. | Fix the request; don't retry as-is. | | `401` | `unauthenticated` | Missing/invalid/expired key. | Check the `Authorization` header. | | `403` | `insufficient_scope` | Key lacks the scope. | Use a higher-scoped key. | | `404` | `not_found` | No such resource. | Verify the id. | | `409` | `conflict` | Duplicate (e.g. slug) or idempotency-key reuse with a different body. | Use a new key / resolve the duplicate. | | `422` | `validation_failed` | Schema/credential/connectivity check failed. | Read `details.check` and fix. | | `429` | `rate_limited` | Too many requests. | Honor `Retry-After`; back off. | | `501` | `not_implemented` | Endpoint not shipped in this phase. | Check [availability](/guide/roadmap). | | `5xx` | `internal` | Server-side error. | Retry with backoff — safe if you sent an idempotency key. | ## Pagination List endpoints are **cursor-paginated** (never offset). Pass `next_cursor` back as `cursor` until it's `null`: ```bash curl "https://api.emithook.com/v1/events?cursor=$NEXT" \ -H "Authorization: Bearer $EK_KEY" ``` ```json { "data": [ /* … */ ], "next_cursor": "eyJ…" } ``` Page size defaults to 50 (`limit` up to 100). Cursors are opaque and short-lived. More in [conventions → pagination](/reference/conventions#pagination). ## Rate limits Limits are per-org, per-key. Every response carries the standard headers; on `429`, wait `Retry-After` seconds: ``` RateLimit-Limit: 600 RateLimit-Remaining: 0 RateLimit-Reset: 30 Retry-After: 30 ``` ## A robust client pattern Put the fundamentals together — authenticate, send an idempotency key, and retry only on transient failures while honoring `Retry-After`: ```ts async function send(body: object) { for (let attempt = 0; attempt < 5; attempt++) { const res = await fetch("https://api.emithook.com/v1/send", { method: "POST", headers: { "Authorization": `Bearer ${process.env.EK_KEY}`, "Content-Type": "application/json", "Idempotency-Key": idemKey, // stable across retries }, body: JSON.stringify(body), }); if (res.ok) return res.json(); if (res.status === 429 || res.status >= 500) { const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt); await new Promise(r => setTimeout(r, wait * 1000)); continue; // safe — same idempotency key } throw new Error((await res.json()).error.message); // 4xx — don't retry } } ``` ## Explore & integrate ::: tip SDKs & codegen The **TypeScript SDK** (`@emithook/sdk`) wraps this API and the Send model and ships today; it powers the [CLI](/reference/cli) and [MCP server](/reference/mcp). **Python and Go SDKs and a Terraform provider are on the [roadmap](/guide/roadmap#what-s-remaining)** — until they ship, generate a client from the [OpenAPI spec](/docs/openapi.yaml). Receivers verify with the open [Standard Webhooks](https://www.standardwebhooks.com/) libraries. ::: --- --- url: /docs/reference/conventions.md --- # API conventions Cross-cutting rules every Emithook API call follows. The [interactive reference](/reference/api) documents each path; this page documents what's true of all of them — the contract an agent or SDK needs before making a single call. ## Base URL & versioning ``` https://api.emithook.com ``` Paths are versioned with a `/v1` prefix. Breaking changes ship under a new prefix; additive changes (new fields, new optional params) do not bump the version, so clients must ignore unknown fields. ## Authentication & scopes Every request carries a scoped API key: ``` Authorization: Bearer ek_live_… ``` Keys are `ek_live_…` (production) or `ek_test_…` (test environment) and carry one of three scopes. Each scope is a superset of the one below it: | Scope | Can | | --- | --- | | `read` | Query events, attempts, metrics, config; read the inbox. Never mutates. | | `write` | Everything in `read` + send, replay, redrive, create/update config, rotate secrets. | | `admin` | Everything in `write` + manage keys, members, domains, billing. | A call requiring more scope than the key holds returns `403 insufficient_scope`. The [CLI](/reference/cli) and [MCP server](/reference/mcp) inherit these scopes exactly. ## Resource IDs IDs are prefixed and ULID-based, so they're globally unique, time-sortable, and self-describing. See the [Glossary ID table](/guide/glossary#id-prefixes) for the full list (`org_`, `ek_`, `ep_`, `dst_`, `app_`, `msg_`, `evt_`, `whsec_`). Treat them as opaque strings of bounded length; don't parse anything but the prefix. ## Idempotency `POST` requests that create or send accept an `Idempotency-Key` header (a UUID you generate): ``` Idempotency-Key: 7e3b… (POST only) ``` Replays with the same key within the window (~12–24 h) return the **original** response (status + body) and do not create a second effect. Use it on `POST /v1/send` and `POST /v1/app/{id}/event` to make retries safe. Distinct from `webhook-id`, which dedupes on the *receiver* side. ## Pagination List endpoints are cursor-paginated (never offset): ```http GET /v1/events?status=failed&cursor=eyJ… ``` ```json { "data": [ { "id": "evt_01JX…" } ], "next_cursor": "eyJ…" // null on the last page } ``` Pass `next_cursor` back as `cursor` to fetch the next page. Cursors are opaque and short-lived. Page size defaults to 50 (`limit` up to 100). ## Errors Non-2xx responses share one envelope: ```json { "error": { "type": "validation_failed", "message": "destination connectivity check failed: 403 from target", "request_id": "req_01JX…", "details": { "check": "connectivity" } } } ``` Always log `request_id` — it correlates to the [event/attempt logs](/guide/glossary#logs-archive-replay) end to end. | HTTP | `type` | Meaning | | --- | --- | --- | | 400 | `bad_request` | Malformed JSON or parameters. | | 401 | `unauthenticated` | Missing/invalid/expired key. | | 403 | `insufficient_scope` | Key lacks the required scope. | | 404 | `not_found` | No such resource. | | 409 | `conflict` | e.g. duplicate slug, or idempotency-key reuse with a different body. | | 422 | `validation_failed` | Schema/credential/connectivity check failed (see `details.check`). | | 429 | `rate_limited` | See rate-limit headers below. | | 501 | `not_implemented` | Feature not yet shipped in this phase (see [Availability](#availability)). | | 5xx | `internal` | Retry with backoff; safe if you sent an idempotency key. | ## Rate limits Per-org, per-key. Every response carries the standard headers; on `429`, honor `Retry-After`: ``` RateLimit-Limit: 600 RateLimit-Remaining: 0 RateLimit-Reset: 30 Retry-After: 30 ``` ## Delivery semantics (canonical) These are the values the engine uses and that this documentation treats as authoritative: * **Guarantee:** at-least-once. Consumers dedupe on `webhook-id`. * **Timeout:** success = a `2xx` within **15 s**. * **Retry schedule:** exponential backoff with full jitter, **8 attempts over ~28 h** — `immediate, 5s, 5m, 30m, 2h, 5h, 10h, 10h`. Per-endpoint configurable (max attempts + schedule). * **DLQ:** after the final attempt; events are replayable for the domain's retention window. * **Circuit breaker:** opens after consecutive hard failures, probes, auto-closes; `410 Gone` disables immediately; `Retry-After`/`429/502/503/504` honored. * **Replays** are flagged `webhook-replayed: true`. ::: tip Receiver requirement Verify against the **raw** request body, not re-parsed JSON — the #1 cause of signature failures. See [signing](/guide/concepts#signing). ::: ## Availability Emithook is pre-release (`v0.1`); surfaces ship by phase. The [Glossary legend](/guide/glossary#availability-legend) maps each badge to a phase. Endpoints not yet shipped return `501 not_implemented`. Today the management + Send API is targeted for Phase 2; queue/pull/email paths and the app-fan-out are Phase 3 / Roadmap. Don't assume a path is live because it appears in the spec — check the badge on the resource. --- --- url: /docs/reference/operations/sendWebhook.md --- --- --- url: /docs/reference/operations/sendAppEvent.md --- --- --- url: /docs/reference/operations/listDestinations.md --- --- --- url: /docs/reference/operations/createDestination.md --- --- --- url: /docs/reference/operations/validateDestination.md --- --- --- url: /docs/reference/operations/createEndpoint.md --- --- --- url: /docs/reference/operations/setEndpointSecret.md --- --- --- url: /docs/reference/operations/listEvents.md --- --- --- url: /docs/reference/operations/getEvent.md --- --- --- url: /docs/reference/operations/replayEvent.md --- --- --- url: /docs/reference/operations/redriveDlq.md --- --- --- url: /docs/reference/operations/getMetrics.md --- --- --- url: /docs/reference/operations/listDomains.md --- --- --- url: /docs/reference/operations/addDomain.md --- --- --- url: /docs/reference/operations/verifyDomain.md --- --- --- url: /docs/reference/operations/pullEvents.md --- --- --- url: /docs/reference/operations/listAliasMessages.md --- --- --- url: /docs/guide/getting-started.md --- # Quickstart Go from zero to a delivered, signed webhook in a few minutes. No DNS, no infrastructure. ::: tip Zero setup A new organization is live immediately on the shared **ingest domain** (`` — a placeholder; the real name is config-driven) — both your webhook URLs and email aliases work right away, with TLS and DKIM already in place. Bringing your own domain is optional (see [Custom domains](#bring-your-own-domain-optional)). ::: ## 1. Get an API key Create an organization, then copy a key from **Settings → API Keys** in the console. Keys are scoped (`read`, `write`, `admin`). ```bash export EK_KEY="ek_live_xxxxxxxxxxxxxxxx" ``` ## 2. Create a destination A **destination** is where delivered events go — an HTTPS URL or a queue. Define it once; reference it by id everywhere. ```bash curl -X POST https://api.emithook.com/v1/destinations \ -H "Authorization: Bearer $EK_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "acme-orders", "type": "https", "url": "https://api.acme.in/hooks/orders" }' ``` ```json // → 201 Created — pending validation { "id": "dst_01JX9...", "name": "acme-orders", "validation": "pending" } ``` A destination is created `pending` and must pass three checks — **schema**, **credentials**, and a **connectivity test** — before it can receive traffic (`validation` becomes `valid`). For queue destinations, the console's guided flow hands you the exact IAM/queue policy to grant. See [Concepts → Destinations](/guide/concepts#destinations-the-central-registry). ## 3. Send your first event ```bash curl -X POST https://api.emithook.com/v1/send \ -H "Authorization: Bearer $EK_KEY" \ -H "Idempotency-Key: inv_001" \ -H "Content-Type: application/json" \ -d '{ "destination": "dst_01JX9...", "event_type": "invoice.created", "payload": { "id": "INV-2026-001", "amount": 4999, "currency": "INR" } }' ``` ```json // → 202 Accepted { "message_id": "msg_01JX9..." } ``` The event is now signed, queued, and delivered with automatic retries. Watch it land in **Delivery Logs** in the console, or tail it from the CLI: ```bash npm i -g @emithook/cli emithook logs tail --destination acme-orders ``` ## 4. Verify the signature (receiver side) Every outbound delivery is signed with the open [Standard Webhooks](https://www.standardwebhooks.com/) scheme (`webhook-id`, `webhook-timestamp`, `webhook-signature`). Verify it with any off-the-shelf library: ```ts import { Webhook } from "standardwebhooks"; const wh = new Webhook(process.env.WHSEC); // whsec_… from the destination app.post("/hooks/orders", (req, res) => { const event = wh.verify(req.rawBody, req.headers); // throws if invalid handle(event); res.sendStatus(200); }); ``` ## Bring your own domain (optional) Want webhooks on your own hostname or email on your own domain? Add it under **Domains** — Emithook issues the DNS records (CNAME for webhooks via Cloudflare for SaaS, MX + SPF/DKIM/DMARC for email via SES) and verifies automatically. Until then, everything runs on the shared ingest domain. ## Next steps * [Core concepts](/guide/concepts) — endpoints, destinations, signing, retries. * [Receive a Shopify webhook](/guides/receive-shopify) — a full inbound walkthrough. * [API reference](/reference/api) — every endpoint, with a Postman collection. --- --- url: /docs/guide/concepts.md --- # Core concepts For exact, term-by-term definitions and the object hierarchy, see the [Glossary & data model](/guide/glossary). This page is the prose tour. ## Endpoints An **endpoint** is an inbound entry you configure and hand to a provider — a slug under the dedicated ingest domain (`/`), a queue source you write to, or an email alias. Endpoints carry the provider preset (signature verification — applied in the processing plane as a verdict, not at the edge — plus the synchronous handshake the provider expects, answered at the edge) and the routes that fan an incoming request out to one or more destinations. ## Destinations & the central registry A **destination** is where a delivered event goes: an HTTPS URL, a queue (SQS, SNS, Pub/Sub, Azure Service Bus, RabbitMQ/AMQP, Kafka, NATS, Redis Streams), or the Pull API. Destinations live in a **central registry** — defined once, referenced by id from many endpoints. Editing a destination updates every reference. Every destination is **validated before it can receive traffic**: 1. **Schema** — the config matches the adapter's JSON schema. 2. **Credentials** — the secret resolves and authenticates. 3. **Connectivity** — a live test publish succeeds. For queue/stream destinations, Emithook publishes using its **own managed identity** — you grant access with a copy-paste snippet (an SQS queue policy, IAM binding, Kafka ACL, etc.) and never share long-lived keys. ## Delivery semantics * **Retries** — exponential backoff with full jitter — 8 attempts over ~28 h (`immediate, 5s, 5m, 30m, 2h, 5h, 10h, 10h`, per-endpoint configurable), then a replayable **dead-letter queue**. Success = a `2xx` within 15 s. See [API conventions → delivery semantics](/reference/conventions#delivery-semantics-canonical). * **Circuit breaker** — repeated failures pause a dead endpoint, park events, then probe and recover. * **Idempotency** — send with an `Idempotency-Key`; duplicates within the window collapse to one delivery. * **SSRF protection** — outbound HTTP resolves DNS, validates the resolved IP against a private/metadata denylist, pins the connection to that IP (defeating DNS-rebinding), and follows no redirects (`3xx` = failure). ## Signing Outbound webhooks use the [Standard Webhooks](https://www.standardwebhooks.com/) spec. The signed content is `{id}.{timestamp}.{body}`, HMAC-SHA256 with a `whsec_` secret, sent as `webhook-id`, `webhook-timestamp`, and `webhook-signature`. Secrets rotate with an overlap window so in-flight messages keep verifying. ## Organizations & roles A user can belong to **multiple organizations** with a different role in each (`Admin`, `Developer`, `Viewer`). You pick one at sign-in and switch anytime from the console. Endpoints, destinations, domains, members, billing and region are all scoped per organization. ## Logs, archive & replay Every attempt is logged with full request/response detail. Logs are written to an **hourly gzipped archive** per URL for backup and export, and any event can be **replayed** — singly, in bulk, or from the archive. --- --- url: /docs/guide/glossary.md --- # Glossary & data model The precise vocabulary of Emithook — the terms the [API](/reference/api), [CLI](/reference/cli) and [MCP server](/reference/mcp) all use. If you are an agent building context before driving Emithook, read this page first: every other surface assumes these definitions. ## Availability legend Emithook's v1 engine is **feature-complete** — the terms and endpoints below are shipped and callable. The few surfaces still on the roadmap are tagged so you don't call something that doesn't exist yet: * Shipped — callable today via the API, CLI and MCP server. * Not yet shipped — Python/Go SDKs, the Terraform provider, a second AWS region, and optional runtime CF→SQS failover. See the [roadmap](/guide/roadmap). Roadmap surfaces return `not_implemented`. Treat the badge as the source of truth for what you can call today. ## The object hierarchy Everything is scoped to an organization and resolves down to a single delivery attempt: ``` Organization ← billing, members, region, API keys └─ Environment ← stage/region isolation (e.g. live, test) ├─ Endpoint ← an INBOUND entry you hand to a provider (relay) │ └─ Route ← filter/transform + the destinations to fan out to ├─ Application ← a SEND customer's end-customer (multi-tenant fan-out) │ └─ Endpoint ← that app's subscribed URL/queue + signing secret ├─ Destination ← a reusable delivery target (the central registry) └─ Domain ← a custom hostname / email domain you've verified ↓ every event flows through ↓ Message / Event → Attempt(s) → delivered | retrying | dlq ``` A subtlety worth internalizing: **"Endpoint" means two different things** depending on direction. On the relay side an Endpoint is *inbound* (a URL a provider posts to). In the Send application model an Endpoint is *outbound* (a subscriber URL belonging to an Application). They are distinct objects with distinct IDs; the docs always say "inbound endpoint" or "application endpoint" when context is ambiguous. ## Core terms ### Directions & products **Relay (receive)** — the inbound product. A provider (Shopify, Stripe, Meta…) sends to a URL/queue/email address Emithook owns; Emithook acks fast, verifies, and routes to your systems. **Send (emit)** — the outbound product (webhooks-as-a-service). You call the Send API with a payload and a destination and Emithook delivers it. Same delivery engine as relay; only the *ingress* differs. ### Ingress (how an event enters) **Dedicated ingest domain** — both webhooks (`/`) and email (`@in.`) live on a **separate registrable domain from the marketing/console site**, for a hard cookie boundary off the auth domain plus reputation/abuse isolation. `` is a placeholder: the real name is an operator choice and the edge reads it from config (it is domain-agnostic). **HTTPS edge** — a provider POSTs to `/` (path-based) on the [dedicated ingest domain](#ingress-how-an-event-enters). The immortal edge **acks and durably buffers in <100 ms** and never rejects on the accept path, so an accepted event is never dropped; signature verification runs later, in the **processing plane**, as a verdict (see *Inbound verification* below). **Queue ingestion** — you produce messages to your own broker (SQS/SNS, Pub/Sub, Azure SB/Event Hubs, Kafka, AMQP, NATS, Redis Streams) and Emithook *consumes* them. Each message becomes an event. Removes per-HTTP-request cost for high-volume senders. **Inbound email / MX engine** — mail to `@in.` is parsed (headers, bodies, attachments) into an event after SPF/DKIM/DMARC checks. Email becomes a webhook. **Send API** — `POST /v1/send`. The fourth ingress: a direct API call. ### Routing & delivery **Destination** — *where a delivered event goes*: an HTTPS URL, a queue, or the Pull API. The unit of the **central registry**: defined once, referenced by id from many endpoints/routes. Editing it updates every reference. ID prefix `dst_`. **Central destination registry** — the single source of truth for outbound targets. A registry entry is `{id, name, adapter type, connection config, credential ref, delivery policy, signing, owner, version}`. No destination is configured inline per route. **Destination validation** — a destination is gated through three checks before it can receive traffic, and **cannot activate until all pass**: (1) **schema** — config matches the adapter's JSON schema; (2) **credentials** — the secret resolves and authenticates; (3) **connectivity** — a live test publish/reachability probe succeeds. Re-run hourly for drift; states `valid | stale | invalid`. **Route** — attaches to an inbound endpoint: an optional filter (header/path/JSON-path match) and an optional sandboxed JS **transformation**, plus the list of destination ids to fan out to. **Fan-out** — one inbound event delivered to N destinations, each signed, retried and logged independently. **Adapter** — the pluggable driver for one broker/storage backend. A queue adapter implements a common interface (`connect/auth`, `publish-with-confirm`, backpressure, health probe) and declares capabilities (ordering, transactions, max message size, dedup). The same adapter serves both outbound destinations and inbound queue ingestion. All broker adapters are . **Managed identity grant** — Emithook publishes to your queue using *its own* identity; you apply a copy-paste grant on your side (SQS queue policy, `gcloud … add-iam-policy-binding`, Kafka ACL, NATS authorization, etc.) rather than sharing long-lived keys. The connectivity check fails with a permission error until the grant is applied. ### Delivery semantics **At-least-once** — every acked event is delivered one or more times. Consumers **must** dedupe on `webhook-id`. **Retry schedule** — exponential backoff with full jitter. Default: **8 attempts over ~28 h** — `immediate, 5s, 5m, 30m, 2h, 5h, 10h, 10h`. Per-endpoint configurable (attempts + schedule). Success = a `2xx` within a 15 s timeout. **DLQ (dead-letter queue)** — where an event lands after the final failed attempt. DLQ events are visible as "failed — replayable" and can be redriven singly or in bulk. **Circuit breaker** — per-destination. Opens after N consecutive hard failures (`5xx`/timeout), parks events instead of burning attempts, probes half-open, auto-closes; the owner is notified on open/close. A `410 Gone` disables a destination immediately; `Retry-After` and `429/502/503/504` are honored. **Idempotency** — on the Send API, an `Idempotency-Key` header (UUID, `POST` only) collapses duplicate sends within a ~12–24 h window to one delivery. End-to-end, `webhook-id` = the ingest event id and is stable across retries. **Claim-check** — bodies over 128 KB are offloaded to object storage (S3/R2) and a pointer is enqueued; the router rehydrates the body. Hard cap 10 MB. **SSRF gate** — every outbound HTTP delivery is guarded: HTTPS + standard ports only, encoded-IP literals and `@` userinfo rejected, DNS resolved then the **resolved IP** validated against a private/metadata denylist (`169.254.169.254`, `127.0.0.0/8`, `10/8`, `172.16/12`, `192.168/16`, `fc00::/7`, …) and the connection **pinned** to that IP (defeats DNS-rebinding), redirects never followed (`3xx` = failure), internal headers stripped. ### Signing & verification **Standard Webhooks** — the on-the-wire spec Emithook signs with, verbatim. Receivers verify with any off-the-shelf library. **Signature headers** — `webhook-id`, `webhook-timestamp` (Unix seconds), `webhook-signature`. The signed content is exactly `{id}.{timestamp}.{body}` (raw bytes sent, not re-serialized) → HMAC-SHA256 → base64 → `v1,`. Optional Ed25519 variant `v1a`. **Signing secret** — per-destination, format `whsec_` (24–64 bytes). **Rotation** signs with current + previous keys (space-delimited in the header) for a 24 h overlap, so in-flight messages keep verifying. **Inbound verification (preset)** — runs in the **processing plane** (not at the edge — the edge accepts and buffers first), per inbound endpoint: Shopify `X-Shopify-Hmac-Sha256`, Stripe `Stripe-Signature` (300 s tolerance), Slack `v0=` (5-min replay window), Meta `X-Hub-Signature-256`, generic HMAC → a **verdict** (mirroring the SES email model). A verified event is routed; a **bad signature is quarantined** (durable, inspectable, **never delivered** — not a `401`, not silently dropped); an **unknown endpoint is dropped** (with a metric, no per-event row); an **inactive/paused endpoint is parked** (durable, replayable). **Provider preset** — a bundle of {signature scheme + expected synchronous response (e.g. Meta `hub.challenge` echo, Slack `url_verification`) + sane retry defaults} selected when you create an inbound endpoint. ~6 at launch, expanding toward ~30. ### Send model entities **Environment** — region/stage isolation (e.g. `live`, `test`). The top container under an org. **Application** — *the Send customer's end-customer*, addressable by the customer's own id via a `uid` (so they integrate statelessly). Holds endpoints, event-type subscriptions, and per-app signing. Can be auto-created on first send. ID prefix `app_`. **Application endpoint** — a URL/queue belonging to an Application, with an event-type filter and its own signing secret. **Message** — one event: an event-type, content, and an optional `eventId`. ID prefix `msg_`. **Attempt** — one delivery try for a message to one destination: request, response, timing, attempt number, status. **EventType** — a schema-bearing identifier (e.g. `invoice.created`) that powers the **event catalog** customers subscribe to. **Three delivery patterns** — (1) **static fan-out**: one event → fixed destinations; (2) **tenant-scoped routing**: `app_id` = tenant id, `POST /v1/app/{id}/event` resolves that tenant's endpoints; (3) **direct send**: `POST /v1/send` names the destination explicitly. ### Logs, archive & replay **Event / attempt log** — every ingest record and every delivery attempt, queryable by tenant/endpoint/status/time/event-id. Bodies stored inline (MongoDB), default 30-day TTL, per-domain retention control (7/30/90 d, or "metadata only"). **Hourly archive** — a scheduled job rolls each endpoint's records for the clock hour into a gzipped NDJSON file (`…///YYYY/MM/DD/HH.jsonl.gz`, `.done` marker per hour), keyed by **attempt-time**; join on `eventId` to reassemble an event and its attempts. Written before TTL prunes the hot store, so it is the durable backup of record. Default 13-month retention. **Replay** — re-deliver an event: single, bulk-by-filter, or **from the archive** for events past the hot window. Replays are flagged (`webhook-replayed: true`) so consumers can distinguish them. **Pull API** — for consumers behind a firewall who can't expose an HTTPS URL: poll `GET /v1/pull/?cursor=…`, cursor-based, batch up to 100, explicit ack advances the cursor. Availability window = the domain's retention setting. ### Identity, access & tooling **Organization** — the top-level tenant: members, roles, billing, region, API keys, all scoped data. A user can belong to multiple orgs with a different role in each. ID prefix `org_`. **Role** — `Admin`, `Developer`, `Viewer`, per org. **API key** — scoped (`read`, `write`, `admin`), format `ek_live_…` / `ek_test_…`. Every API/CLI/MCP call is gated by key scope. ID/prefix `ek_`. **Management API** — the one REST/JSON surface (`api.emithook.com`) that everything else is a thin client over. The console, CLI and MCP server have no private backdoor. **CLI** — `@emithook/cli` (`emithook …`): the full operational + query surface over the management API. See the [CLI reference](/reference/cli). **MCP server** — exposes the management API as Model Context Protocol tools so agents can query and operate Emithook. Read tools are safe by default; write tools require a write-scoped key / explicit confirmation. Includes the email inbox tools. See the [MCP reference](/reference/mcp). **Consumer portal** — an embeddable, white-label view a Send customer embeds for *their* end-customers (magic-link/JWT scoped to one Application, no Emithook account): manage endpoints, view logs, replay, browse the event catalog, rotate the secret, read the email inbox. **Agent inbox** — the LLM-readable side of the MX engine: give an agent its own address (`agent-42@in.`) and it reads/acts on its mail through MCP tools (`list_emails`, `get_email`, `search_emails`, `summarize_thread`). ## ID prefixes Resource ids are prefixed and (mostly) ULID-based, so they're time-sortable and self-describing: | Prefix | Resource | | --- | --- | | `org_` | Organization | | `ek_` | API key (`ek_live_…`, `ek_test_…`) | | `ep_` | Inbound endpoint | | `dst_` | Destination (registry entry) | | `app_` | Application (Send model) | | `msg_` | Message (a sent/relayed event) | | `evt_` | Event (ingest record) | | `whsec_` | Signing secret | ## See also * [Core concepts](/guide/concepts) — the same ideas in prose, with examples. * [API conventions](/reference/conventions) — IDs, errors, pagination, scopes, idempotency. * [API reference](/reference/api) · [CLI](/reference/cli) · [MCP server](/reference/mcp) --- --- url: /docs/guide/receive.md --- # 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 `/`. Acked + durably buffered at the edge in <100 ms (never dropped); verified in the processing plane. | | | **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. | | | **Inbound email (MX)** | Mail to `@in.` is parsed (headers, bodies, attachments) into an event after SPF/DKIM/DMARC checks. | | | **Send API** | A direct API call — see [Send](/guide/send). | | Webhooks and email share one **dedicated ingest domain** (``), kept separate from the marketing/console site for a hard cookie boundary off the auth domain plus reputation isolation. `` is a placeholder — the real name is an operator choice and config-driven (the edge is domain-agnostic). See the [glossary](/guide/glossary#ingress-how-an-event-enters) 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 ``. A [custom domain](/guide/getting-started#bring-your-own-domain-optional) 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](/guide/concepts#destinations-the-central-registry). A route can: * **Filter** — header/path/JSON-path match to drop or conditionally route events. * **Transform** — reshape/enrich/redact the payload with sandboxed JS, versioned and testable against a saved sample. Unroutable events are never dropped — they're parked and become replayable once a route exists. ## 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](/reference/conventions#delivery-semantics-canonical). ## Tutorials * [Receive a Shopify webhook](/guides/receive-shopify) — a complete inbound walkthrough: verify, fan out to an API and a queue, replay. ## Next * [Send webhooks](/guide/send) — the outbound half of the engine. * [Destinations & validation](/guide/concepts#destinations-the-central-registry) — where events go. * [API reference](/reference/api) · [CLI](/reference/cli) · [MCP server](/reference/mcp) --- --- url: /docs/guides/receive-shopify.md --- # Receive a Shopify webhook A complete inbound walkthrough: take Shopify `orders/create` webhooks, verify them, and fan them out to your API and a queue — losing nothing if your service is down. ## What you'll build ``` Shopify ──▶ /shopify-store/orders ──▶ ├─ HTTPS https://api.acme.in/orders (acked + buffered <100ms; HMAC verified in processing) └─ SQS acme-orders-q ``` ## 1. Create the endpoint Use the **Shopify** preset — it configures HMAC-SHA256 verification and the `200 < 5s` response Shopify expects. ```bash curl -X POST https://api.emithook.com/v1/endpoints \ -H "Authorization: Bearer $EK_KEY" \ -H "Content-Type: application/json" \ -d '{ "slug": "shopify-store", "path": "/orders", "preset": "shopify", "destinations": ["dst_acme_https", "dst_acme_sqs"] }' ``` ```json // → 201 Created { "id": "ep_01JX9...", "url": "https:///shopify-store/orders", "verification": "hmac-sha256", "status": "active" } ``` ::: tip The URL is live immediately on the shared ingest domain (``, config-driven) — no DNS. Point Shopify at it under **Settings → Notifications → Webhooks**. ::: ## 2. Add the verification secret Paste the signing secret Shopify shows you so Emithook can verify the `X-Shopify-Hmac-Sha256` header in the processing plane: ```bash curl -X PUT https://api.emithook.com/v1/endpoints/ep_01JX9.../secret \ -H "Authorization: Bearer $EK_KEY" \ -d '{ "secret": "shpss_..." }' ``` The edge accepts and durably buffers every request in <100 ms (never dropped); verification then runs in the processing plane. A request that fails verification is **quarantined** — durable and inspectable, but never delivered (not a `401` at the edge, never silently dropped). ## 3. What an incoming request looks like ```http POST /shopify-store/orders HTTP/1.1 Host: X-Shopify-Topic: orders/create X-Shopify-Hmac-Sha256: 9q8Wd...= Content-Type: application/json { "id": 8201, "total_price": "4999.00", "currency": "INR", "email": "buyer@acme.in" } ``` ```http HTTP/1.1 200 OK { "received": true } ``` Emithook acks in under 100 ms, then fans the event out to both destinations independently — each signed, retried, and logged on its own. ## 4. Confirm delivery ```bash emithook logs tail --endpoint /shopify-store/orders ``` ``` 12:04:31 evt_01JX… orders/create → dst_acme_https 200 142ms ✓ 12:04:31 evt_01JX… orders/create → dst_acme_sqs enqueued ✓ ``` If `api.acme.in` is down, Emithook retries with backoff and parks events behind the circuit breaker — when it recovers, they drain automatically. Nothing is lost. ## 5. Replay if needed ```bash # replay everything that dead-lettered for this endpoint emithook replay --dlq --endpoint /shopify-store/orders ``` ## See also * [Concepts → Destinations & validation](/guide/concepts#destinations-the-central-registry) * [API reference](/reference/api) --- --- url: /docs/guide/send.md --- # Send webhooks — including the application/subscription model & the consumer portal. The **Send** side: emit your own events as webhooks-as-a-service. You don't run delivery infrastructure — Emithook signs, retries, circuit-breaks, logs, and archives every send through the *same* engine the relay uses. Only the ingress differs. ## Three ways to resolve a destination All three share signing, retry/backoff, the per-destination circuit breaker, SSRF protection, DLQ, and logs. They differ only in *how the target is chosen*: | Pattern | Use it when | How it resolves | | --- | --- | --- | | **Direct send** | One-off / system-to-system push, no event mapping. | `POST /v1/send` names a registered destination id or a validated HTTPS URL. | | **Static fan-out** | An event always goes to the same place(s). | An event type → a fixed route set. | | **Per-tenant (applications)** | A multi-tenant product where each customer wants their own URL/queue for the same event. | `app_id` = your end-customer's id; `POST /v1/app/{id}/event` resolves *that* tenant's endpoints. | ## Direct send The simplest path — one call delivers one payload: ```bash curl -X POST https://api.emithook.com/v1/send \ -H "Authorization: Bearer $EK_KEY" \ -H "Idempotency-Key: inv_001" \ -H "Content-Type: application/json" \ -d '{ "destination": "dst_01JX9...", "event_type": "invoice.created", "payload": { "id": "INV-2026-001", "amount": 4999, "currency": "INR" } }' ``` ```json // → 202 Accepted { "message_id": "msg_01JX9..." } ``` Send `Idempotency-Key` so client retries can't double-deliver. See the [Quickstart](/guide/getting-started) for the end-to-end version. ## Queue and broker destinations A destination doesn't have to be an HTTPS URL — it can be a **message broker**. The same engine (signing context, retry/backoff, per-destination circuit breaker, DLQ, logs) applies; only the final hop changes: instead of an HTTP `POST`, the event is **published** to the broker. One adapter class per broker implements the same `QueuePort` (publish *and* consume), so a broker can be both a delivery target and an ingestion source. Every adapter passes the *same* shared `QueuePort` conformance suite (publish/receive, ack removes, nack redelivers with an incrementing delivery count, FIFO within a group, `consume()` auto-ack, health): | `type` | Broker | FIFO within a group | Delivery count | Conformance | | --- | --- | --- | --- | --- | | `sqs` | AWS SQS | FIFO queues (`MessageGroupId`) | `ApproximateReceiveCount` | CI (ElasticMQ) | | `nats` | NATS JetStream | stream order | JetStream redelivery count | CI | | `redis` | Redis Streams | stream order | `XCLAIM` delivery count | CI | | `amqp` | RabbitMQ | queue order | quorum-queue `x-delivery-count` | CI (RabbitMQ) | | `kafka` | Apache Kafka | per-key partition order | adapter-tracked per offset | CI (Redpanda) | | `pubsub` | Google Pub/Sub | message ordering (`orderingKey`) | `deliveryAttempt` (via DLQ policy) | CI (emulator) | | `azure` | Azure Service Bus | queue order | `deliveryCount` | conformance-ready — **not CI-verified** | ::: tip Azure Service Bus The Azure adapter is implemented against the same contract and ships with the identical conformance suite, but there is no lightweight Service Bus emulator that supports dynamic queue creation, so the suite is **gated on `AZURE_SERVICEBUS_TEST_URL` and skipped in CI**. Point that variable at a real namespace to run the full contract end to end. ::: Broker connection details live in the destination's stored configuration, not in environment variables — only the self-host **ingest/delivery backbone** (`QueuePort`) is env-configured. See [self-hosting](/guide/self-hosting). ## The application / subscription model Turn Emithook into "webhooks for your product." Register each of your end-customers as an **Application** (keyed by *your* own id), each holding **endpoints** (URL/queue + event-type filter + its own signing secret). Then fan out: ```bash curl -X POST https://api.emithook.com/v1/app/cust_8f2a/event \ -H "Authorization: Bearer $EK_KEY" \ -d '{ "event_type": "invoice.created", "payload": { "id": "INV-1" } }' ``` ```json // → 202 Accepted — fanned out to every matching endpoint { "message_id": "msg_01JX9...", "fanout": 3 } ``` Applications can be lazily auto-created on first send. The data model (Environment → Application → Endpoint, plus Message/Attempt/EventType) is defined in the [glossary](/guide/glossary#send-model-entities). ## Consumer portal Your end-customers manage their own endpoints, view delivery logs, replay failures, browse the event catalog, and rotate their secret — in an **embeddable, white-label portal** scoped to one application via a magic link, with no Emithook account. This is what offloads webhook support from your team to theirs. ## Receiving side (your customers) Every delivery is signed with [Standard Webhooks](https://www.standardwebhooks.com/), so your customers verify with any off-the-shelf library — verifying against the **raw** body. See [signing](/guide/concepts#signing). ## Next * [API conventions](/reference/conventions) — idempotency, scopes, errors, retry schedule. * [Receive webhooks](/guide/receive) — the inbound half. * [API reference](/reference/api) · [MCP server](/reference/mcp) --- --- url: /docs/reference/cli.md --- # CLI The `emithook` CLI is a thin client over the same scoped [management API](/reference/conventions) (via `@emithook/sdk`) — it never speaks HTTP directly, so the console, CLI and MCP server stay in lockstep. JSON or line output, scriptable for CI and runbooks. ::: tip v0.1 scope The shipped command set is the operational core: **`send`, `logs` (with `--follow` to tail), `replay`, `dlq redrive`, and `keys current`** — documented below. Richer resource management (endpoint/destination/domain CRUD, profiles, archive download) is driven through the [API](/reference/api), [console](/guide/concepts#organizations-roles), or [MCP server](/reference/mcp) today and is tracked on the [roadmap](/guide/roadmap). ::: ## Install & authenticate ```bash npm i -g @emithook/cli export EMITHOOK_API_KEY=ek_live_… # required; every command reads it export EMITHOOK_BASE_URL=https://api.emithook.com # optional (self-host override) ``` There is no `login` step or stored profile — the CLI is stateless and key-driven, which is what makes it drop into CI unchanged. ## Commands ### Send ```bash emithook send --type --payload \ [--idempotency-key ] [--json] # example emithook send dst_acme --type invoice.created \ --payload '{"id":"INV-1","amount":4999}' --idempotency-key inv_001 ``` `` is a registered destination id or a validated HTTPS URL. Prints `queued ` (or the raw JSON with `--json`). ### Logs (and tail) ```bash emithook logs [--status delivered|failed|retrying|dlq] [--endpoint ] \ [--event-type ] [--since ] [--until ] [--limit ] [--follow] [--json] # one-shot emithook logs --status failed --endpoint ep_razorpay --since 2026-06-01T00:00:00Z # tail live events (polls; prints each event once) emithook logs --endpoint ep_shopify --follow ``` Without `--json` each row is ` `; a `… more (cursor …)` line is printed when the page is truncated. ### Replay ```bash emithook replay # re-deliver one event (flagged webhook-replayed) ``` ### DLQ redrive ```bash emithook dlq redrive [--endpoint ] [--since ] [--json] # bulk-replay dead-lettered events ``` Prints `redriven ` (or JSON). ### Keys ```bash emithook keys current # inspect the LOCAL key: masked value + env (live/test) ``` `keys current` only inspects the key in `EMITHOOK_API_KEY` — it never prints the full secret and does not call a key-management API (key lifecycle is console/API-side in v0.1). ## Output & scripting Add `--json` to `send`, `logs` and `dlq redrive` for machine output; combine with `jq`: ```bash emithook logs --status dlq --json | jq -r '.data[].id' ``` Paginated lists return the envelope `{ "data": [...], "next_cursor": "…" }`; pass `--limit` and re-issue with no cursor flag yet (cursor paging is API-side — see [API conventions](/reference/conventions#pagination)). ## Exit codes | Code | Meaning | | --- | --- | | `0` | Success. | | `1` | Runtime / API error (the API's `type: message` is printed to stderr). | | `2` | Usage error (missing/invalid arguments). | The same operations are available through the [API](/reference/api) and the [MCP server](/reference/mcp), so terminal, code, and AI agents share one model. See the [Glossary](/guide/glossary) for the entity names used in flags. --- --- url: /docs/reference/mcp.md --- # MCP server The Emithook **MCP server** (`@emithook/mcp`) exposes the [management API](/reference/conventions) as [Model Context Protocol](https://modelcontextprotocol.io) tools, so an AI assistant (Claude, Cursor, an internal agent) can **query** and **operate** Emithook in natural language — "show failed deliveries to `ep_razorpay` in the last hour", "redrive the DLQ for that endpoint", "create an endpoint with the Shopify preset". Every tool is a thin wrapper over the same scoped key (via `@emithook/sdk`) — there is no capability here the API doesn't have. If you are an agent, read the [Glossary](/guide/glossary) before calling tools: the arguments below use those exact entity names. ## Connect The server runs locally over **stdio** and talks to the API with your key: ```jsonc // add to your MCP client config (e.g. Claude Desktop) { "mcpServers": { "emithook": { "command": "npx", "args": ["-y", "@emithook/mcp"], "env": { "EMITHOOK_API_KEY": "ek_live_…", "EMITHOOK_SCOPE": "read" } } } } ``` | Env var | Purpose | | --- | --- | | `EMITHOOK_API_KEY` | **Required.** The scoped key the tools act under. | | `EMITHOOK_SCOPE` | Operator-declared scope (`read` | `write` | `admin`, default `read`) — caps which tools are exposed. Never widens the key; the API enforces the real scope regardless. | | `EMITHOOK_BASE_URL` | API base override (self-host). | | `EMITHOOK_INBOX_MONGO_URL` + `EMITHOOK_ORG_ID` | Enable the inbox tools (read the MX `emails` store for that org). `EMITHOOK_INBOX_MONGO_DB` optional. Absent ⇒ inbox tools report "inbox not configured". | ## Auth, scopes & safety * **Read tools are safe by default.** A `read`-scoped key can call them; they never mutate state. * **Write tools require a `write` (or `admin`) key** *and* an **explicit confirmation**: the registry appends a `confirm` boolean to every write tool's schema, so the call is a no-op until the client passes `confirm: true`. With a `read` scope the write tools aren't exposed at all. * Tools inherit the key's organization; an agent can never reach across orgs. | Tool class | Min scope | Confirmation | | --- | --- | --- | | `list_*`, `get_*`, `search_*`, `summarize_thread`, `get_metrics` | `read` | none (safe) | | `send_webhook`, `replay_event`, `redrive_dlq`, `create_endpoint`, `create_destination`, `rotate_endpoint_secret` | `write` | `confirm: true` required | ## Tool catalog ### Query (read-safe) | Tool | Arguments | Returns | | --- | --- | --- | | `list_events` | `status?` (`delivered\|failed\|retrying\|dlq`), `endpoint?`, `event_type?`, `since?`, `until?`, `cursor?`, `limit?` | A page of events. | | `get_event` | `id` | One event with all its delivery attempts. | | `list_destinations` | `type?`, `validation?`, `cursor?`, `limit?` | Registry destinations. | | `list_domains` | — | Custom domains with verification state + DNS records. | | `get_metrics` | `key` (an endpoint **or** destination id) | Pre-aggregated rollups for that id. | ### Email inbox (agent-native) The headline of the MX engine: hand an agent its own address and let it reason over its mail. All read-safe; they read the MX `emails` store directly and activate only when `EMITHOOK_INBOX_MONGO_URL` + `EMITHOOK_ORG_ID` are set (otherwise they report "inbox not configured"). | Tool | Arguments | Returns | | --- | --- | --- | | `list_emails` | `alias`, `cursor?`, `limit?` | An alias's messages, newest first — from/to, subject, received, auth result, attachments, parsed text body. | | `get_email` | `message_id` | One email in full: headers + parsed body + SPF/DKIM/DMARC result. | | `search_emails` | `query`, `alias?`, `limit?` | Newest-first matches across subject / from / body, optionally within one alias. | | `summarize_thread` | `alias`, `subject` | The alias's messages sharing that subject (Re:/Fwd: stripped), oldest first, for the model to summarize. Returns the thread — it does not itself summarize. | ### Operate (write — `confirm: true` required) | Tool | Arguments | Effect | | --- | --- | --- | | `send_webhook` | `destination`, `event_type`, `payload`, `headers?`, `idempotency_key?` | Send one webhook to a destination id or HTTPS URL. | | `replay_event` | `id` | Re-deliver a single event (flagged `webhook-replayed`). | | `redrive_dlq` | `endpoint?`, `since?` | Bulk-replay dead-lettered events. | | `create_endpoint` | `slug`, `path?`, `preset?`, `destinations?[]` | Create an inbound endpoint. | | `create_destination` | `name`, `type`, `url?`, `config?` | Register an outbound destination (created pending validation). | | `rotate_endpoint_secret` | `id`, `secret` | Set an endpoint's inbound verify secret (write-only, never echoed). | ## Example session ```text Agent: "What's failing for ep_razorpay in the last hour?" → get_metrics(key="ep_razorpay") → list_events(endpoint="ep_razorpay", status="failed", since="2026-06-22T05:00:00Z") → get_event(id="evt_01JX…") # inspect the 5xx body Agent: "Replay the dead-lettered ones." → redrive_dlq(endpoint="ep_razorpay", confirm=true) # ⚠ write — needs confirm ``` ## See also * [API conventions](/reference/conventions) — the scopes, IDs and errors these tools share. * [CLI](/reference/cli) — the same operations from a terminal. * [Glossary](/guide/glossary) — entity definitions used in every argument. --- --- url: /docs/guide/security.md --- # Security How Emithook keeps inbound events authentic, outbound deliveries safe, and payloads private. ## Inbound authenticity Every inbound endpoint's signature is verified using the [provider preset](/guide/receive#endpoints-provider-presets): Shopify, Stripe (300 s tolerance), Slack (`v0=`, 5-min replay window), Meta, Razorpay, or generic HMAC. Per the founding invariant (ADR-0021), the **immortal edge accepts and durably buffers every request in <100 ms and never drops an accepted event** — it does *not* verify or `401` on the accept path. Verification runs in the **processing plane** as a verdict: a verified event is routed and delivered; a **bad signature is quarantined** — durable and inspectable, but **never delivered**; an unknown endpoint is dropped (with a metric); an inactive/paused endpoint is parked (durable, replayable). Unauthenticated events are therefore still never delivered — the guarantee is unchanged; only *where* verification happens moved (off the accept path, so a flood of bad signatures can never threaten ingest availability). Optional per-endpoint IP allowlists and edge WAF/rate-limiting add defense in depth. For [inbound email](/guide/receive), the analogue is **SPF / DKIM / DMARC** verification, recorded on each event with a configurable drop/quarantine policy. ## Outbound signing Outbound webhooks are signed with the [Standard Webhooks](https://www.standardwebhooks.com/) spec, verbatim: * Headers `webhook-id`, `webhook-timestamp` (Unix seconds), `webhook-signature`. * Signed content is exactly `{id}.{timestamp}.{body}` (the raw bytes sent), HMAC-SHA256 → base64 → `v1,`. * Per-destination secret `whsec_…`. **Rotation** signs with current + previous keys (space-delimited) for a 24 h overlap, so in-flight messages keep verifying. Receivers verify with any off-the-shelf library — against the **raw** request body (the #1 cause of verification failures). ## SSRF protection — the highest-severity outbound control Because customers (and, in the Send platform, *their* end-customers) supply arbitrary destination URLs, the delivery path is a Server-Side Request Forgery surface — the class behind the Capital One breach via the cloud metadata endpoint. Emithook applies these controls **at delivery time, not just at registration**: * HTTPS and standard ports only; reject encoded-IP literals and `@` userinfo. * Resolve DNS through a public resolver, validate the **resolved** IP against the reserved/private denylist (`169.254.169.254`, `127.0.0.0/8`, `10/8`, `172.16/12`, `192.168/16`, `169.254/16`, `fc00::/7`), then connect to that **pinned** IP — defeating DNS-rebinding (TOCTOU). * Never follow redirects (`3xx` = failure). * Strip internal headers/credentials. * Route all delivery egress through a Smokescreen-style proxy in an isolated subnet with a default-deny network policy that cannot reach internal services. Defense in depth = an app-layer IP check **and** network-layer egress isolation. This is a launch gate, not a hardening nice-to-have. ## Data residency & retention Payloads at rest stay in **your selected region** — the managed cloud is multi-region by design (ADR-0020), with India (`ap-south-1`) available today and more regions coming — excepting transient edge buffering during a regional failover (documented in the DPA). Encrypted at rest (SSE/KMS). Retention is a [per-domain control](/guide/glossary#logs-archive-replay) (7/30/90 d, or "metadata only" to null payloads for sensitive endpoints); the hourly archive has its own longer lifecycle (default 13 months) then hard delete. ## Access control Scoped [API keys](/reference/conventions#authentication-scopes) (`read ⊂ write ⊂ admin`) gate every API/CLI/MCP call. Per-org RBAC (`Admin`/`Developer`/`Viewer`), optional 2FA/MFA and SSO/SAML, an active-sessions view with per-session revoke, and an audit log of console actions. Least-privilege IAM per service; secrets in a secrets manager, never inline. ## Next * [API conventions](/reference/conventions) — scopes, idempotency, errors. * [Self-hosting](/guide/self-hosting) — running it in your own environment. --- --- url: /docs/guide/self-hosting.md --- # Self-hosting Emithook is **Apache-2.0, open-core**: the complete engine — ingest, router, delivery, all adapters, console, CLI, MCP server, and SDKs — is open source and genuinely self-hostable. You can run the whole thing with **no AWS or Cloudflare account** — the one exception is inbound email (SES today; see the caveat below). ::: warning Pre-release The engine and its container images ship with the roadmap phases. This page describes the intended self-host model from the architecture; treat it as the design contract, not a download link yet. ::: ## Ports & adapters The TypeScript/Node core depends only on a handful of interfaces — never on a cloud SDK directly. AWS and Cloudflare are just *one* set of implementations; swapping them is configuration, not a fork. | Port | Managed (our cloud) | Self-host default | | --- | --- | --- | | `QueuePort` | AWS SQS | NATS JetStream (or Redis Streams) | | `StoragePort` | S3 / R2 | MinIO | | `LogStorePort` | MongoDB | MongoDB, or single-Postgres mode | | `CachePort` | ElastiCache Redis | Redis / Valkey | | `SecretsPort` | AWS Secrets Manager | Vault / SOPS / env | | ConfigDB | Postgres (RDS) | Postgres (container) | The same hard logic (verify, sign, route, backoff, SSRF guard) ships as one shared `@emithook/core` package, run two ways — as a serverless handler in our cloud, and as a long-running worker in a container — so there is never a second implementation that can drift. ::: warning Inbound email needs AWS (SES) today Webhooks (HTTPS edge + queue ingestion) and Send are **fully self-hostable** with no AWS or Cloudflare account. **Inbound email is the one exception:** the MX engine currently parses mail from an Amazon **SES** inbound feed, so self-hosting inbound email today means pointing the MX worker at **your own SES** (your AWS account, your region). The receiver sits behind an `InboundEmailPort` seam, so a provider-agnostic path — your own SMTP receiver, or a Cloudflare Email Workers adapter — can drop in without touching the consumer; that adapter is on the [roadmap](/guide/roadmap#what-s-remaining). If you don't need inbound email, none of this applies. ::: ## Quickstart (Docker Compose) A first-class requirement, not an afterthought: one command brings up a working instance. ```bash docker compose up # brings up: emithook + Postgres + NATS + Redis + MinIO ``` That gives you ingest, routing, delivery, the console, CLI and MCP server against local infrastructure. An optional Bun/Node single-binary build is provided for bare-metal convenience, and a Helm chart for Kubernetes. ## What's managed-only Self-hosters get a genuinely complete product, not a crippled teaser. The hosted service adds operational value that isn't required to run Emithook yourself: multi-region / DR, the hosted global edge, our SLA, the hosted consumer portal at scale, and usage billing. ## Spec-compatible on the wire Outbound signing follows [Standard Webhooks](https://www.standardwebhooks.com/) verbatim, so receivers and tooling stay portable — you are never locked to a self-host *or* the managed service. ## Next * [Security](/guide/security) — the SSRF gate, signing, residency. * [Concepts → Destinations](/guide/concepts#destinations-the-central-registry) — the adapter framework. * [Roadmap & availability](/guide/roadmap) — what ships when. --- --- url: /docs/guide/roadmap.md --- # Roadmap & availability Emithook's v1 engine is **feature-complete**. Both products (relay + Send), all four ingresses, the console, the management/Send API, the full CLI, the MCP server, the TypeScript SDK, every broker adapter, and the application/subscription model + consumer portal are **shipped**. This page is the single source of truth for what you can call today; if you're an agent, read it before driving the API, CLI or MCP server. ## Availability legend The same badges appear throughout the docs: * Shipped — you can call it today. * Not yet shipped — see [What's remaining](#what-s-remaining) below. Roadmap surfaces return `not_implemented` until they land. ## What's shipped Everything below is live: * **Core engine** — Cloudflare Worker edge (presets, handshakes, custom domains) as a pure accept-always buffer → durable queue, with verify-as-verdict in the processing plane. One Fair-Queue delivery path with backoff/jitter, circuit breaker, [Standard Webhooks signing](/guide/security#outbound-signing), idempotency, claim-check, SSRF gate. Hardened event store, hourly per-endpoint archive, logs, replay (incl. restore/replay from archive), and the Pull API. * **All four ingresses** — the [HTTPS edge](/guide/receive), [queue ingestion across every broker adapter](/guide/concepts#destinations-the-central-registry) (SQS/SNS, Pub/Sub, Azure Service Bus/Event Hubs, AMQP, Kafka/Redpanda, NATS, Redis Streams), [inbound email (the MX engine + agent inbox)](/guide/receive), and the [Send API](/guide/send). * **Tenant experience** — the [console](/guide/concepts#organizations-roles) (dashboard, endpoint wizard, log explorer, replay, secrets, alerts), self-service onboarding, custom domains, filters + sandboxed JS transformations, and the expanding provider-preset catalog. * **Send** — the [Send API](/guide/send), the [application/subscription model + consumer portal](/guide/send), FIFO endpoints, and the [queue-adapter framework in both directions](/guide/concepts#destinations-the-central-registry). * **Tooling** — the full [CLI](/reference/cli), the [MCP server](/reference/mcp) (query + write + the [LLM-readable inbox tools](/reference/mcp#email-inbox-agent-native)), and the [TypeScript SDK](/reference/api) (`@emithook/sdk`). ## What's remaining The short list of surfaces still on the roadmap: * **Python & Go SDKs** — the TypeScript SDK ships today; Python and Go are next. Until then, generate a client from the [OpenAPI spec](/reference/api) (the interim codegen path). * **Terraform provider** — declarative management of endpoints/destinations/keys. * **A second AWS region** — for multi-region / DR beyond the current single managed region. * **Optional runtime CF→SQS failover** — a configurable runtime failover of the ingest buffer from Cloudflare Queues to SQS. Off by default (it re-introduces a cross-cloud dependency on the accept path); SQS already ships as a selectable deploy-time buffer. * **The inbound-email front door** — inbound email ships today on SES; whether the platform default moves to Cloudflare Email Workers (with SES retained for residency) is an open positioning decision. The path is behind the `InboundEmailPort` seam, so it's an adapter choice, not a rewrite. ## Surface status at a glance | Surface | Status | | --- | --- | | HTTPS ingest, signing, retries, DLQ, logs, archive, replay, Pull API | | | Console, management API, Send API (`/v1/send`) | | | CLI, MCP server, TypeScript SDK | | | Queue destinations & ingestion (all brokers) | | | Application fan-out (`/v1/app/{id}/event`), consumer portal | | | Inbound email + agent inbox | | | Python / Go SDKs, Terraform provider | | | Second AWS region, optional runtime CF→SQS failover | | Changelog: [github.com/emithook/emithook/releases](https://github.com/emithook/emithook/releases). --- --- url: /docs/reference/api-explorer.md --- # API explorer moved The interactive API reference now lives at **[API operations](/reference/operations/)** — every endpoint in the sidebar, grouped by area with method badges. Redirecting you there now…