CLI
AvailableThe emithook CLI is a thin client over the same scoped management API (via @emithook/sdk) — it never speaks HTTP directly, so the console, CLI and MCP server stay in lockstep. Every command reads and writes JSON; add --json to any read command for machine output that pipes straight into jq and CI runbooks.
The full command surface is live: send, endpoint/destination/domain management, event and request inspection, inbound-email routing, observability, and an api escape hatch for anything the typed commands don't wrap yet.
Start here: emithook guide
One command that prints the whole surface — auth, the output contract, exit codes, pagination, idempotency, a worked example per resource, and the RCA and reporting workflows:
emithook guideIt is written for an agent that has run nothing else, and its command inventory is generated from the CLI itself, so it cannot fall behind what ships (a test asserts every registered command appears in it). If you are pointing an AI assistant at Emithook for the first time, this is the one thing to have it read.
Install & authenticate
npm i -g @emithook/cliAuthenticate one of two ways. Interactively, emithook login runs an OAuth device grant — it prints a code, opens the console to confirm it, and stores the resulting session locally:
emithook login # device-grant flow against the console
emithook whoami # confirm the active org + identityFor CI and scripts, set a scoped API key instead — EMITHOOK_API_KEY always wins over a stored login session, so the same command runs unchanged in a pipeline:
export EMITHOOK_API_KEY=ek_live_… # scoped key; overrides any login session
export EMITHOOK_BASE_URL=https://api.emithook.com # optional: management API base (self-host override)
export EMITHOOK_CONSOLE_URL=https://app.emithook.com # optional: console base used by the login device grantEMITHOOK_BASE_URL points the SDK at a self-hosted management API; EMITHOOK_CONSOLE_URL points the login device grant at the matching console. Keys are ek_live_…/ek_test_… and carry the same read/write/admin scopes as everywhere else.
Command model
Commands are noun-verb — a resource group (endpoints, destinations, events, domains, aliases, auth) followed by an action. The common operational verbs also have flat aliases kept for muscle memory and short scripts: logs (= events list), replay (= events replay), login/logout/whoami (= auth …), keys current (= auth keys current).
emithook guide # the whole surface in one page — read this first
emithook help # list every command group
emithook help endpoints # drill into one group
emithook endpoints --help # same, via the --help flag on any commandAdd --json to any read command to get the raw API envelope instead of the human table. IDs are the prefixed, ULID-based strings from the glossary (ep_, dst_, evt_, dom_, msg_, …) — pass them verbatim.
Send
# to a registered destination or a validated HTTPS URL
emithook send <destination> --type <event_type> --payload <json> [--idempotency-key <key>]
# fan out to every destination bound to an application
emithook send app <app_id> --type <event_type> --payload <json>
# example
emithook send dst_acme --type invoice.created \
--payload '{"id":"INV-1","amount":4999}' --idempotency-key inv_001--idempotency-key makes a retried send safe — the same key inside the window returns the original result (see idempotency).
Outbound email
Send transactional email and inspect its delivery. See Send email.
emithook email send --from <addr> --to <addr[,addr]> --subject <s> \
(--html <h> | --text <t>) [--attach <path[,path]>] [--attach-url <url[,url]>] \
[--idempotency-key <key>]
emithook email list [--status <s>] [--since <t>] [--until <t>] [--cursor <c>] [--limit <n>]
# example
emithook email send --from no-reply@acme.send.emithook.com \
--to buyer@example.com --subject "Your invoice" --text "Thanks for your order."
emithook email list --status bounced --limit 20--to takes one or more comma-separated recipients. --from's domain must match a verified sending identity. --status is one of sending/delivered/bounced/complained/suppressed/failed/dlq.
--attach reads each file locally and base64-encodes it; --attach-url hands a public https URL to Emithook to fetch at accept time. Both are comma-separated, and share one limit: 20 attachments and 10 MB decoded per request. See Send email.
Endpoints
Manage the inbound entries you hand to providers.
emithook endpoints create --url <receive-url> \
[--preset <name>] [--destinations <csv>] [--response-rules <json>]
emithook endpoints get <id>
emithook endpoints update <id> [--url <receive-url>] \
[--preset <name>] [--clear-preset] [--status active|paused] \
[--destinations <csv>] [--response-rules <json>] [--clear-response-rules]
emithook endpoints delete <id>
emithook endpoints set-secret <id> --secret <s>
# example
emithook endpoints create --url https://wh.emhk.in/acme/shopify-prod --preset shopify \
--destinations dst_warehouse,dst_analyticsYou create an endpoint by its receive URL — the URL you hand to the provider, and the endpoint's identity. The host must be one of your ingest domains, and the URL must start with that domain's prefix (emithook ingest-domains list shows it): on a shared platform host that includes your org slug (https://wh.emhk.in/<org-slug>/…). Any number of path segments may follow; matching is exact, ignoring letter case and a trailing slash.
On update, --url moves the endpoint to that URL. Use --clear-preset / --clear-response-rules to remove a value, and --status paused to stop accepting deliveries without deleting the endpoint.
Destinations
The central outbound registry.
emithook destinations list [--type <t>] [--validation <v>] [--cursor <c>] [--limit <n>]
emithook destinations create --name <name> --type <type> [--url <url>] [--config <json>]
emithook destinations validate <id>
# example
emithook destinations create --name Warehouse --type https \
--url https://api.acme.dev/hooks
emithook destinations validate dst_warehouse # runs the connectivity/credential checkEvents & requests
events are delivered outbound events and their attempts; requests are the raw inbound webhooks the front door received (including unknown/dropped ones). Both are replayable, and both share the DLQ.
emithook events list [--status <s>] [--endpoint <id>] [--event-type <t>] \
[--since <iso>] [--until <iso>] [--cursor <c>] [--limit <n>] [--follow] # alias: logs
emithook events get <id>
emithook events replay <id> # alias: replay
emithook requests list [--endpoint <id>] [--outcome <o>] [--since <iso>] \
[--until <iso>] [--cursor <c>] [--limit <n>]
emithook requests get <id>
emithook requests replay <id>
emithook dlq redrive [--endpoint <id>] [--since <iso>] # bulk-replay dead-lettered events
emithook endpoints replay <id> --since <iso> --until <iso> [--status <s>] \
[--destinations <d1,d2>] (--dry-run | --confirm) # start a replay of a time range
emithook replays list [--endpoint <id>] [--limit <n>] # replays, newest first
emithook replays get <id> # progress of one replay
emithook replays stop <id> # stop between pages
emithook replays resume <id> # continue a stopped/failed one
# tail failing deliveries for one endpoint (polls; prints each event once)
emithook events list --status failed --endpoint ep_razorpay --follow
# bulk-replay everything that dead-lettered since a cutoff
emithook dlq redrive --endpoint ep_shopify --since 2026-07-01T00:00:00Z
# fill a newly added SQS destination with the morning's backlog: preview, then replay
emithook endpoints replay ep_shopify --since 2026-09-18T00:00:00Z --until 2026-09-18T06:00:00Z \
--destinations dst_sqs --dry-run
emithook endpoints replay ep_shopify --since 2026-09-18T00:00:00Z --until 2026-09-18T06:00:00Z \
--destinations dst_sqs --confirm--follow turns events list into a live tail; replays are flagged webhook-replayed: true on the receiver.
Domains
Verify custom webhook and email domains.
emithook domains list
emithook domains add <domain> [--kind webhook|email|email-send]
emithook domains update <id> --kind <email|email-send> [--acknowledge-mx-conflict]
emithook domains verify <id>
emithook ingest-domains list # the shared platform ingest domains you can attach to
# example
emithook domains add hooks.acme.dev --kind webhook
emithook domains add notify.acme.dev --kind email-send # sends only; no MX, never receives
emithook domains verify dom_acme # re-check DNS after adding the records--kind email receives mail (and can additionally be upgraded to send); --kind email-send sends only — no MX is issued, no alias can route on it, and it verifies on ownership + DKIM alone. See Send-only email domains.
domains update converts between those two in place. It is refused while an alias still receives on the domain, and converting back to email publishes an MX that redirects the domain's mail — hence --acknowledge-mx-conflict. See Switching between receiving and send-only.
Inbound email
Route inbound email through platform or custom-domain aliases, then read the received messages, raw MIME, and attachments.
emithook aliases list [--cursor <c>] [--limit <n>]
emithook aliases create --kind platform|custom --local-part <lp> [--domain-id <id>] \
[--destinations <csv>] [--allowed-senders <csv>] [--max-size-bytes <n>]
emithook aliases get <id>
emithook aliases update <id> [--destinations <csv>] [--allowed-senders <csv>] \
[--clear-allowed-senders] [--max-size-bytes <n>] [--clear-max-size]
emithook aliases delete <id>
# read the mailbox for an alias
emithook aliases messages <alias> [--cursor <c>]
emithook aliases messages get <alias> <messageId>
emithook aliases messages raw <alias> <messageId> [--out <file>]
emithook aliases messages attachment <alias> <messageId> <attachment> [--out <file>] # att_… id or index
# re-deliver an alias's emails from a time range to its current destinations
emithook aliases replay <alias> --since <iso> --until <iso> [--status <s>] (--dry-run | --confirm)
# example
emithook aliases create --kind custom --local-part support --domain-id dom_acme \
--destinations dst_helpdesk --allowed-senders '@acme.com'
emithook aliases messages support --cursor eyJ…
emithook aliases messages attachment al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7 evt_01M50GHMJRP15Z4602V9XFVKDA \
att_01M50GHMJRP15Z4602V9XFVKDA_0 --out invoice.pdf
emithook aliases replay al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7 \
--since 2026-10-15T00:00:00Z --until 2026-10-15T23:59:59Z --status failed --dry-runOmit --out to stream the raw message or attachment to stdout; provide it to write the bytes to a file.
Secrets
Named secrets your configuration references as ${secret.NAME} — a destination's custom headers, for instance — instead of holding the credential itself.
emithook secrets list # names + metadata (values are never readable)
emithook secrets get <id>
emithook secrets create <name> --value-env <VAR> # or --value-file <path> ("-" for stdin)
emithook secrets update <id> --value-env <VAR> # rotate in place; --name / --description too
emithook secrets delete <id> --confirm
# example
SLACK_TOKEN=xoxb-… emithook secrets create slack_token --value-env SLACK_TOKEN
emithook secrets create api_key --value-file ./key.txt --description "Partner API key"The value is deliberately not a flag: an argument is visible in shell history and in every ps listing, so it comes from an env var or a file instead. Values are write-only — no command here can print one back. Rotating with secrets update takes effect on the next use everywhere the secret is referenced, with nothing to re-edit; a name that is still referenced cannot be renamed or deleted (the API refuses and names the referents).
Writing requires an admin key. The whole resource is deliberately absent from the MCP surface — an agent can neither read your credential inventory nor plant a credential.
Observability
emithook metrics get <key> # pre-aggregated per-endpoint / per-destination rollup
emithook endpoints stats <id> [--window 24h|7d|30d] # an endpoint's performance summary
emithook endpoints stats-embed <id> [--window w] [--theme auto|light|dark] [--ttl <s>] # signed status-page link
emithook pull <endpoint> [--cursor <c>] [--limit <n>] # cursor-based pull delivery for firewalled consumers
# example
emithook metrics get ep_shopify --json
emithook pull ep_firewalled --limit 100Auth
emithook auth login # alias: login
emithook auth logout # alias: logout
emithook auth whoami # alias: whoami — active org + identity
emithook auth keys current # alias: keys current — inspect the active key (masked value + env)auth keys current inspects the credential in use and never prints the full secret. auth logout clears the stored login session; it does not touch EMITHOOK_API_KEY (unset that env var yourself).
Shell completion
Generate a completion script for your shell and load it. Emit to stdout, then install per your shell's convention:
# zsh — write into a directory on your $fpath, then restart the shell
emithook completion zsh > "${fpath[1]}/_emithook"
# bash
emithook completion bash > /etc/bash_completion.d/emithook
# fish
emithook completion fish > ~/.config/fish/completions/emithook.fishapi passthrough
For any endpoint the typed commands don't wrap yet, api sends a raw authenticated request through the same client and prints the response:
emithook api <METHOD> <path> [--data <json>] [--query <qs>] [--json]
# example
emithook api GET /v1/endpoints
emithook api GET /v1/events --query 'status=failed&limit=10'
emithook api POST /v1/endpoints --data '{"url":"https://wh.emhk.in/acme/x"}'It carries your active credentials and base URL, so you never re-plumb auth. Use --json for the unformatted body.
Output & scripting
Add --json to any read command for machine output, then pipe through jq:
emithook events list --status dlq --json | jq -r '.data[].id'Paginated lists return the envelope { "data": [...], "next_cursor": "…" }; pass next_cursor back via --cursor (with --limit up to 100) to page — see pagination.
Exit codes
| Code | Meaning |
|---|---|
0 | Success. |
1 | Runtime / API error (the API's error.message is printed to stderr). |
2 | Usage error (missing/invalid arguments). |
The same operations are available through the API and the MCP server, so terminal, code, and AI agents share one model. See the Glossary for the entity names used in flags.