npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mittr/sdk-node

v0.2.0

Published

Official Node.js SDK for Mittr — webhooks, AI agent actions, and the audit trail over them

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

Quickstart

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