@mittr/sdk-node
v0.2.0
Published
Official Node.js SDK for Mittr — webhooks, AI agent actions, and the audit trail over them
Maintainers
Readme
@mittr/sdk-node
Official Node.js SDK for Mittr — a thin, typed wrapper around the REST API plus a webhook signature verifier.
Mittr delivers the actions your software takes, whether your own code fires them, an agent decides them, or a provider sends them to you. This SDK covers both directions plus the audit trail over them.
- Zero runtime dependencies (uses built-in
fetch+node:crypto) - Node 18+
- ESM, full TypeScript types
- Same on-the-wire format as raw HTTP — see Pricing
Install
npm install @mittr/sdk-node
pnpm add @mittr/sdk-node
yarn add @mittr/sdk-nodeQuickstart
import { Mittr } from "@mittr/sdk-node";
const mittr = new Mittr({ apiKey: process.env.MITTR_API_KEY! });
const event = await mittr.events.send({
eventType: "order.created",
payload: { orderId: "ord_123", amount: 9900 },
});
console.log(event.id, event.status);The constructor accepts an optional baseUrl (defaults to https://app.mittr.io) and an optional fetchImpl for runtimes where the global fetch is missing or sandboxed.
Sending events
events.send() auto-generates an idempotency key per call. Override it when you have a stable upstream id — that way your own retries don't double-charge or double-deliver:
await mittr.events.send(
{ eventType: "order.created", payload },
{ idempotencyKey: `order-${order.id}-created` },
);Paginating
list() returns one page. iterate() walks every page for you:
const page = await mittr.events.list({ status: "failed", limit: 100 });
console.log(page.data, page.pagination);
for await (const ev of mittr.events.iterate({ status: "failed" })) {
await mittr.events.replay(ev.id);
}Same shape on mittr.endpoints.list() / mittr.endpoints.iterate().
Agent actions
Tag a send with agentRunId and every action from that run becomes queryable as one unit, each with its delivery attempts inline. Untagged actions are delivered normally but never appear in the ledger.
await mittr.events.send({
eventType: "invoice.issued",
payload,
agentRunId: run.id,
agentMetadata: { framework: "langgraph", step: "notify" },
});
const { data } = await mittr.ledger.agentActions({ agentRunId: run.id });
for (const action of data) {
console.log(action.destination, action.outcome, action.attemptCount);
}sendBatch applies run-level correlation to every item, so dispatching several actions at once doesn't mean repeating the id:
await mittr.events.sendBatch({
agentRunId: run.id,
events: actions.map((a) => ({
idempotencyKey: a.id,
destination: a.url,
payload: a.body,
})),
});Your customers' endpoints
If your product lets your users register webhook URLs, model each of them as a sub-account. They own their endpoints and events, so delivery history stays separate per customer and one can never read another's traffic.
const customer = await mittr.subAccounts.create({
externalId: user.id, // your id — unique per workspace, so a retried signup is idempotent
name: user.company,
});
await mittr.endpoints.create({ url, eventTypes: ["invoice.*"] });To let them manage it themselves, mint a scoped session and hand the token to their browser. Your backend never proxies their calls and they never hold a workspace API key:
const { token } = await mittr.subAccounts.createSession(customer.id, {
scope: ["endpoints:read", "endpoints:write", "events:read"],
});The token is single-use. The browser exchanges it once, which sets an httpOnly cookie, so the link can't be replayed out of a history. Revoke with revokeAllSessions(id) when a customer offboards.
Receiving webhooks from providers
An inbound endpoint is a URL you hand to a third party. Mittr verifies the signature against that platform's scheme, stores the request either way, and fans the payload out to your endpoints.
const inbound = await mittr.inbound.create({
name: "Stripe",
pathSuffix: "stripe-prod",
source: "stripe",
sourceConfig: { signing_secret: process.env.STRIPE_WEBHOOK_SECRET! },
destinationIds: [endpoint.id],
});Omitting sourceConfig on update leaves the stored credentials alone, so renaming an endpoint never means resending the secret. Pass {} to clear them.
Verifying inbound webhooks
verifyWebhook is a standalone function — no Mittr instance required. Always pass the raw request body. With Express, capture the raw body before any JSON parser touches it:
import express from "express";
import { verifyWebhook } from "@mittr/sdk-node";
const app = express();
app.post(
"/webhooks/mittr",
express.raw({ type: "*/*" }),
(req, res) => {
const ok = verifyWebhook(
process.env.MITTR_WEBHOOK_SECRET!,
req.headers,
req.body, // Buffer when express.raw is used
);
if (!ok) return res.status(401).send("invalid signature");
const event = JSON.parse(req.body.toString("utf8"));
// ... handle event ...
res.status(200).send("ok");
},
);The verifier rejects signatures whose timestamp is more than 5 minutes from now by default. Tune with toleranceSeconds:
verifyWebhook(secret, req.headers, req.body, { toleranceSeconds: 60 });Errors
Every non-2xx response throws MittrError with the parsed body and status code:
import { MittrError } from "@mittr/sdk-node";
try {
await mittr.events.send({ payload: null! });
} catch (err) {
if (err instanceof MittrError && err.status === 400) {
console.error("bad input:", err.body);
} else {
throw err;
}
}Pricing
The SDK is free and open-source (MIT). Calls through the SDK are billed identically to raw HTTP — there's no SDK markup and no SDK discount. mittr.events.send(...) lands on the same POST /api/v1/events and counts against your plan's monthly event quota exactly the same way. Overage rates apply identically.
The one indirect win: events.send() defaults to a fresh idempotency key per call, so your own internal retries don't double-bill or double-deliver.
API surface
new Mittr({ apiKey, baseUrl?, fetchImpl? })
mittr.events
.send(input, { idempotencyKey? }) // POST /api/v1/events
.list(params?) // GET /api/v1/events
.iterate(params?) // async iterator across pages
.get(id) // GET /api/v1/events/:id
.replay(id) // POST /api/v1/events/:id/replay
mittr.events
.sendBatch(input) // POST /api/v1/events/batch
mittr.endpoints
.create(input) // POST /api/v1/endpoints
.list(params?) // GET /api/v1/endpoints
.iterate(params?) // async iterator across pages
.get(id) // GET /api/v1/endpoints/:id
.update(id, input) // PATCH /api/v1/endpoints/:id
.delete(id) // DELETE /api/v1/endpoints/:id
mittr.inbound
.create(input) // POST /api/v1/inbound-endpoints
.list(params?) // GET /api/v1/inbound-endpoints
.iterate(params?) // async iterator across pages
.get(id) // GET /api/v1/inbound-endpoints/:id
.update(id, input) // PATCH /api/v1/inbound-endpoints/:id
.delete(id) // DELETE /api/v1/inbound-endpoints/:id
.presets() // GET /api/v1/inbound-presets
mittr.subAccounts
.create(input) // POST /api/v1/sub-accounts
.list(params?) // GET /api/v1/sub-accounts
.iterate(params?) // async iterator (offset-paged)
.get(id) // GET /api/v1/sub-accounts/:id
.update(id, input) // PATCH /api/v1/sub-accounts/:id
.deactivate(id) // DELETE /api/v1/sub-accounts/:id
.createSession(id, input?) // POST /api/v1/sub-accounts/:id/sessions
.listSessions(id) // GET /api/v1/sub-accounts/:id/sessions
.revokeSession(id, sessionId) // DELETE /api/v1/sub-accounts/:id/sessions/:sid
.revokeAllSessions(id) // DELETE /api/v1/sub-accounts/:id/sessions
mittr.ledger
.agentActions(params?) // GET /api/v1/ledger/agent-actions
verifyWebhook(secret, headers, rawBody, { toleranceSeconds?, now? })Versioning
Pre-1.0; the public surface may change between minor versions. The signature wire format is pinned by signature.test.ts against the canonical Go signer in pkg/signature/hmac.go and won't drift silently.
License
MIT
