TypeScript SDK
Available@emithook/sdk is the official TypeScript client for the scoped management API — the same client the CLI and MCP server are built on, so all three surfaces stay in lockstep. It is typed end-to-end against the canonical OpenAPI contract, with auth, the error envelope, cursor pagination, idempotency keys, and retries handled in one request path.
It runs on Node.js ≥ 20 (global fetch), ships as ESM with zero runtime dependencies, and is Apache-2.0 like the rest of the monorepo.
Install & authenticate
npm install @emithook/sdkCreate a scoped API key in the console (ek_live_… / ek_test_…, carrying the same read/write/admin scopes as everywhere else) and construct the client with it:
import { EmithookClient } from '@emithook/sdk'
const emithook = new EmithookClient({
apiKey: process.env.EMITHOOK_API_KEY!,
// baseUrl: 'https://api.emithook.com' // default; point at your self-hosted API instead
})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' },
)An idempotencyKey sets the Idempotency-Key header: the API deduplicates the send, and the SDK is then free to retry it on transient failures. A POST without an idempotency key is never retried — the client won't risk double-sending.
Receive, inspect, replay
// 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',
})
// inspect delivered events, replay one (re-sends to EVERY destination it went to, delivered ones
// too; to retry only the failed destinations, redrive the endpoint's DLQ instead)
const failed = await emithook.listEvents({ status: 'failed', limit: 50 })
await emithook.replayEvent(failed.data[0].id)
// replay a whole time range to the endpoint's CURRENT routes (e.g. a newly added SQS queue)
const job = await emithook.replayEndpointEvents(endpoint.id, {
since: '2026-09-18T00:00:00Z',
until: '2026-09-18T06:00:00Z',
})
// It runs on the server — one call whatever the size. Poll it, and resume it if it fails.
const progress = await emithook.getReplay(job.id)
console.log(progress.state, progress.replayed, 'of', progress.total_estimate)Endpoint stats
// one endpoint's performance over 24h (default), 7d or 30d
const stats = await emithook.getEndpointStats(endpoint.id, { window: '7d' })
// a signed, expiring link to a ready-made status page — mint server-side, hand the URL to the browser
const { url, expires_at } = await emithook.getEndpointStatsEmbed(endpoint.id, { ttl: 3600 })See Endpoint stats & status page.
Verify a delivery
Your own receiver (the server behind an https destination) checks each delivery's Standard Webhooks signature and gets back the typed, already-parsed delivery envelope:
import { verifyDelivery, EmithookWebhookVerificationError } from '@emithook/sdk'
app.post('/emithook', express.raw({ type: '*/*' }), async (req, res) => {
try {
const event = await verifyDelivery<{ amount: number }>({
body: req.body, // raw bytes, exactly as received
headers: req.headers,
secret: process.env.EMITHOOK_SIGNING_SECRET!, // the destination's whsec_…
})
console.log(event.metadata.name, event.data.body.amount)
res.sendStatus(204)
} catch (err) {
if (err instanceof EmithookWebhookVerificationError) return res.sendStatus(401)
throw err
}
})It uses WebCrypto only, so it also runs on Workers, Deno and Bun. Pass several secrets (an array, or one space-delimited string) while you rotate a secret. A raw-mode destination's body isn't an envelope; verify those with any Standard Webhooks library.
Pagination
Every list call returns { data, next_cursor }; pass next_cursor back as cursor, or stream with the async iterators:
for await (const event of emithook.iterateEvents({ status: 'failed' })) {
console.log(event.id, event.event_type)
}
// also: iterateDestinations(), iterateEmails()Errors
Non-2xx responses throw a typed EmithookApiError carrying the standard error envelope:
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
}
}Coverage
Every /v1 operation has a typed method — send, endpoints, destinations, events (+ replay, pull, DLQ redrive), the inbound-request inspector, metrics, domains, outbound email + suppressions, and inbound email aliases — plus a rawRequest() escape hatch that keeps auth and envelope handling for anything not wrapped yet. Shapes come from the API operations reference; the SDK never invents a field. The package README lists the full method table.
Versioning
Pre-1.0: breaking changes bump the minor version (0.1.x → 0.2.0); patch releases are always safe to take. Other languages are planned; until then, any HTTP client works directly against the API reference.