Skip to content

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

  • Environmentek_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:
ScopeGrants
readQuery events, attempts, metrics, config, inbox. Never mutates.
writeread + send, replay, redrive, create/update config, rotate secrets.
adminwrite + 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):

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

CodeMeaning
200 OKRead succeeded; body is the resource or a page.
201 CreatedResource created (e.g. a destination, pending validation).
202 AcceptedWork queued (send, replay, redrive) — message_id returned.
204 No ContentSucceeded, 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:

json
{
  "error": {
    "type": "validation_failed",
    "message": "destination connectivity check failed: 403 from target",
    "request_id": "req_01JX9…",
    "details": { "check": "connectivity" }
  }
}
HTTPtypeWhenWhat to do
400bad_requestMalformed JSON or params.Fix the request; don't retry as-is.
401unauthenticatedMissing/invalid/expired key.Check the Authorization header.
403insufficient_scopeKey lacks the scope.Use a higher-scoped key.
404not_foundNo such resource.Verify the id.
409conflictDuplicate (e.g. slug) or idempotency-key reuse with a different body.Use a new key / resolve the duplicate.
422validation_failedSchema/credential/connectivity check failed.Read details.check and fix.
429rate_limitedToo many requests.Honor Retry-After; back off.
501not_implementedEndpoint not shipped in this phase.Check availability.
5xxinternalServer-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.

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

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.

Apache-2.0 licensed · a Finnoto product