API reference
The Emithook API is one scoped REST/JSON surface — the substrate the console, CLI, SDKs and MCP server 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; to browse every endpoint, open API operations.
Pre-release
Emithook is v0.1; endpoints ship by roadmap phase. Paths not yet live return 501 not_implemented. The management + Send API is targeted for Phase 2.
Base URL & versioning
https://api.emithook.comEvery 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:
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) orek_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).
Make your first call
Two calls — one read, one write — show the whole shape of the API.
List recent events (read):
curl https://api.emithook.com/v1/events?status=failed \
-H "Authorization: Bearer $EK_KEY"// → 200 OK
{
"data": [
{ "id": "evt_01JX9…", "event_type": "orders/create",
"endpoint": "/shopify-store/orders", "status": "failed" }
],
"next_cursor": null
}Send a webhook (write):
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 } }'// → 202 Accepted
{ "message_id": "msg_01JX9…" }The Quickstart 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.
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.
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:
{
"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. |
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:
curl "https://api.emithook.com/v1/events?cursor=$NEXT" \
-H "Authorization: Bearer $EK_KEY"{ "data": [ /* … */ ], "next_cursor": "eyJ…" }Page size defaults to 50 (limit up to 100). Cursors are opaque and short-lived. More in 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: 30A robust client pattern
Put the fundamentals together — authenticate, send an idempotency key, and retry only on transient failures while honoring Retry-After:
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
SDKs & codegen
The TypeScript SDK (@emithook/sdk) wraps this API and the Send model and ships today; it powers the CLI and MCP server. Python and Go SDKs and a Terraform provider are on the roadmap — until they ship, generate a client from the OpenAPI spec. Receivers verify with the open Standard Webhooks libraries.