API conventions
Cross-cutting rules every Emithook API call follows. The interactive reference 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.comPaths 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 and MCP server 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 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):
GET /v1/events?status=failed&cursor=eyJ…{
"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:
{
"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 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). |
| 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: 30Delivery 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
2xxwithin 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 Gonedisables immediately;Retry-After/429/502/503/504honored. - Replays are flagged
webhook-replayed: true.
Receiver requirement
Verify against the raw request body, not re-parsed JSON — the #1 cause of signature failures. See signing.
Availability
Emithook is pre-release (v0.1); surfaces ship by phase. The Glossary 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.