Skip to content

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.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:

ScopeCan
readQuery events, attempts, metrics, config; read the inbox. Never mutates.
writeEverything in read + send, replay, redrive, create/update config, rotate secrets.
adminEverything 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):

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 end to end.

HTTPtypeMeaning
400bad_requestMalformed JSON or parameters.
401unauthenticatedMissing/invalid/expired key.
403insufficient_scopeKey lacks the required scope.
404not_foundNo such resource.
409conflicte.g. duplicate slug, or idempotency-key reuse with a different body.
422validation_failedSchema/credential/connectivity check failed (see details.check).
429rate_limitedSee rate-limit headers below.
501not_implementedFeature not yet shipped in this phase (see Availability).
5xxinternalRetry 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 himmediate, 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.

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.

Apache-2.0 licensed · a Finnoto product