Skip to content

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 ​

bash
npm install @emithook/sdk

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

ts
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 ​

ts
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 ​

ts
// 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 ​

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

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

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

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

Emithook · a Finnoto product