@emithook/sdk
v0.1.0
Published
Typed TypeScript client for the Emithook /v1 API (docs/openapi.yaml): scoped-key auth, the error envelope, cursor pagination, idempotency keys, sensible retries. The one authoritative API client — the CLI and MCP server build on it.
Maintainers
Readme
@emithook/sdk
The official TypeScript client for the Emithook /v1 management API — receive and send webhooks on one engine. Typed end-to-end against the canonical OpenAPI spec, with scoped-key auth, the error envelope, cursor pagination, idempotency keys, and sensible retries built into one request path. The Emithook CLI and MCP server are built on this client.
- Runtime: Node.js ≥ 20 (uses the global
fetch). ESM only. - Dependencies: none.
- Docs: SDK guide · API reference · API conventions
Install
npm install @emithook/sdkAuthenticate
Create a scoped API key (ek_live_… / ek_test_…) in the Emithook console. Keys carry read / write / admin scopes; the SDK sends the key as a Bearer token on every request.
import { EmithookClient } from '@emithook/sdk'
const emithook = new EmithookClient({
apiKey: process.env.EMITHOOK_API_KEY!,
// baseUrl: 'https://api.emithook.com' // default; override for self-host
})Keys are secrets — use the SDK from your server only, never in browser code.
Send a webhook
const { message_id } = await emithook.send(
{
destination: 'https://example.com/hooks/orders', // or a registered destination id
event_type: 'order.created',
payload: { order_id: 'ord_123', total: 4200 },
},
{ idempotencyKey: 'order-created-ord_123' },
)Passing an idempotencyKey sets the Idempotency-Key header and makes the POST safely retryable — the API deduplicates, and the SDK retries it on transient failures.
Receive webhooks
Create an endpoint, point providers at its URL, then inspect and replay what arrives:
// pick an ingest domain, then create the receive URL you hand to the provider
const { data: domains } = await emithook.listIngestDomains()
const endpoint = await emithook.createEndpoint({
url: `https://${domains[0].host}/my-org/shopify-prod`,
preset: 'shopify', // optional provider preset; see the API reference
})
// inspect delivered events
const { data, next_cursor } = await emithook.listEvents({ status: 'failed', limit: 50 })
// replay one
await emithook.replayEvent(data[0].id)The client surface
One method per /v1 operation — flat methods for the core resources, sub-resource objects for the rest:
| Area | Methods |
| --- | --- |
| Send | send, sendAppEvent |
| Endpoints | createEndpoint, getEndpoint, updateEndpoint, deleteEndpoint, setEndpointSecret, listIngestDomains |
| Destinations | listDestinations, createDestination, checkDestination, validateDestination, updateDestination, deleteDestination |
| Events | listEvents, getEvent, replayEvent, pullEvents, redriveDlq |
| Inbound requests | requests.list, requests.get, requests.replay |
| Metrics | getMetrics |
| Domains | listDomains, addDomain, preflightDomain, verifyDomain, updateDomain |
| Email (outbound) | emails.send, emails.sendBatch, emails.get, emails.list |
| Suppressions | suppressions.list, suppressions.create, suppressions.delete |
| Email aliases (inbound) | listAliases, createAlias, getAlias, updateAlias, deleteAlias, listAliasMessages, getAliasMessage, getAliasMessageRaw, getAliasMessageAttachment |
| Escape hatch | rawRequest(method, path, { query, body }) — auth + envelope handling for anything not wrapped yet |
Request/response shapes come from docs/openapi.yaml, the canonical contract — the SDK never invents a field.
Pagination
List calls return a Page<T>: { data, next_cursor }. Pass next_cursor back as cursor to continue, or use the async iterators to stream every item:
for await (const event of emithook.iterateEvents({ status: 'failed' })) {
console.log(event.id, event.event_type)
}
// also: iterateDestinations(), iterateEmails()Errors
Every non-2xx response throws a typed EmithookApiError carrying the API's { error: { type, message, request_id, details } } envelope; network failures throw EmithookError with the underlying cause:
import { EmithookApiError } from '@emithook/sdk'
try {
await emithook.getEndpoint('ep_missing')
} catch (err) {
if (err instanceof EmithookApiError) {
err.type // 'not_found' | 'rate_limited' | 'validation_failed' | …
err.status // 404
err.requestId // quote this when contacting support
err.details // field-level validation errors, when present
}
}Retries
GET requests — and any request carrying an idempotencyKey — are retried automatically on network errors and on 429 / 5xx responses (default 2 retries, exponential backoff, Retry-After respected). A POST without an idempotency key is never retried: the SDK won't risk double-sending. Tune with maxRetries / retryBaseMs, and inject a custom fetch for tests or transports:
const client = new EmithookClient({ apiKey, maxRetries: 4, fetch: myFetch })Versioning
Pre-1.0: breaking changes bump the minor version (0.1.x → 0.2.0); patch releases are always safe to take.
License
Apache-2.0 — part of the open-source Emithook monorepo.
