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

@assertkit/node

v0.6.0

Published

Official Node SDK for AssertKit — wait for emails, extract OTPs, run flows, manage inboxes and custom domains.

Downloads

2,183

Readme

@assertkit/node

Official Node SDK for AssertKit — the QA toolkit for ship-day confidence. Wait for emails in tests without polling, extract OTPs and magic links, run flows from CI, manage inboxes and custom domains.

npm install @assertkit/node

Quick start

import { AssertKit } from "@assertkit/node";

const ak = new AssertKit({ apiKey: process.env.ASSERTKIT_API_KEY });

const inbox = ak.inbox("qa-signup");
// Trigger your signup flow…
await fetch("https://api.your-app.com/signup", {
  method: "POST",
  body: JSON.stringify({ email: inbox.address }),
});
// Then wait for the OTP — returns the moment it arrives.
const otp = await inbox.waitForOtp({ from: "[email protected]" });
console.log("Got OTP:", otp);

Auth

Most operations need an API key from /dashboard/api-keys. Read-only wait/list against the public disposable-mail surface works without one.

// Authenticated (uses /api/v1/*, plan-based rate limits, custom domains)
const ak = new AssertKit({ apiKey: "ak_live_..." });

// Or set ASSERTKIT_API_KEY in your env and just:
const ak = new AssertKit();

// Anonymous (uses /api/public/*, IP rate-limited, default domain only)
const ak = new AssertKit();

Core API

client.inbox(localPart) — fluent inbox handle

const inbox = ak.inbox("qa-signup");
inbox.address;  // "[email protected]"

inbox.wait(filters) — long-poll for next matching message

Returns the moment a matching message arrives. Long-polls up to 25s (configurable) on a single HTTP request — no client-side polling loop.

const r = await inbox.wait({
  from: "[email protected]",   // substring match
  subjectContains: "verify",       // substring match
  kind: "otp",                     // only return if an OTP was extracted
  timeoutMs: 15_000,
  since: new Date(Date.now() - 5 * 60 * 1000), // last 5 min
});

if ("timeout" in r) {
  // No matching mail arrived within the window
} else {
  const otp = r.codes.find(c => c.kind === "otp")?.value;
}

inbox.waitForOtp(filters) — direct OTP value

Convenience over wait(). Throws on timeout or no-OTP-found.

const otp = await inbox.waitForOtp({ from: "noreply", timeoutMs: 10_000 });

inbox.waitForMagicLink(filters) — direct magic-link URL

const link = await inbox.waitForMagicLink({ from: "[email protected]" });
await page.goto(link); // click through to verify

inbox.messages() — list current messages

const msgs = await inbox.messages();
// [{ id, subject, from_addr, snippet, received_at, ... }]

inbox.codes() — all currently-extracted codes

const codes = await inbox.codes();
// [{ kind: "otp", value: "472913", ... }]

inbox.wipe() — clear all messages

Only works on public disposable inboxes (owned/private inboxes are protected — wipe via the dashboard or per-message delete).

const { deleted } = await inbox.wipe();

client.send(input) — outbound email

Admin/Pro+ only. From-allowlist server-validated.

await ak.send({
  fromLocal: "support",
  to: "[email protected]",
  subject: "Welcome",
  text: "Hi there",
  html: "<p>Hi there</p>",
});

client.runFlow({ flowId, waitForCompletion }) — trigger a flow

const run = await ak.runFlow({
  flowId: "signup-smoke",
  waitForCompletion: true,
  timeoutMs: 60_000,
});
if (run.status === "failed") throw new Error("Flow failed");

Test-isolation helper

uniqueLocalPart() generates unique inbox names so parallel runs don't collide:

import { AssertKit, uniqueLocalPart } from "@assertkit/node";

test("signup", async () => {
  const inbox = ak.inbox(uniqueLocalPart("signup"));
  // → "[email protected]" — unique per test
  ...
});

Custom domains

If you've verified a custom MX domain at /dashboard/domains:

const ak = new AssertKit({
  apiKey: process.env.ASSERTKIT_API_KEY,
  domain: "mail.your-company.com",
});
ak.inbox("support").address;  // "[email protected]"

Rate limits + retries

The SDK auto-retries on 429 Too Many Requests and 503 Service Unavailable — up to 3 times by default, honoring the server's Retry-After header (clamped to 10s per retry). Other 4xx/5xx are NOT retried (writes shouldn't retry blindly; client errors won't fix themselves).

const ak = new AssertKit({
  apiKey: "...",
  maxRetries: 3,         // 0 = disable auto-retry
  maxBackoffMs: 10_000,  // cap on a single Retry-After wait
});

After retries are exhausted the AssertKitError surfaces with status: 429 so your code can fall back / circuit-break.

Plan tiers + endpoint requirements

| Operation | Plan required | |---|---| | inbox.wait() / waitForOtp() / waitForMagicLink() | Free (API-key) or anonymous (public surface) | | inbox.messages() / inbox.codes() | Free | | inbox.wipe() | Free (public disposable only — owned/private inboxes refuse) | | client.listInboxes() | Free (API-key required) | | client.getMessage() / deleteMessage() | Free | | client.send() | Pro+ (features.outboundSendApi) | | client.runFlow() | Free — quota: flowRunsPerDay (Free 5 / Pro 100 / Team unlimited) | | Custom-MX domain | Pro+ (features.customDomains) — pass domain in ClientOptions |

When you hit a tier gate, the API returns 402 Payment Required with an error body. The SDK propagates this as AssertKitError with status: 402.

Input validation

The SDK rejects bad inputs locally (before consuming a rate-limit token):

  • Local-parts must match /^[a-z0-9._-]{1,64}$/i. ak.inbox("foo bar") throws.
  • timeoutMs clamped to [250, 25_000].
  • runFlow({ pollIntervalMs }) clamped to [250, 30_000]; max 600 poll iterations (10 min at 1s).
  • maxRetries clamped to [0, 10].
  • maxBackoffMs clamped to [100, 60_000].

Errors

All non-2xx responses throw AssertKitError:

import { AssertKitError } from "@assertkit/node";

try {
  await inbox.waitForOtp({ from: "noreply" });
} catch (e) {
  if (e instanceof AssertKitError && e.status === 429) {
    // rate-limited — back off
  }
  throw e;
}

Common pitfalls

| Pitfall | What happens | Fix | |---|---|---| | Passing a full email to client.inbox() | Now works — auto-splits on @. The embedded domain overrides the client's default for that handle. | client.inbox("[email protected]") ✓ — or client.inbox("support") if you want to use the client's default. | | Passing a full email to send({ fromLocal }) | Throws — fromLocal is just the local-part. | Use the new inbox.send({ to, subject, text }) shortcut to skip the split entirely, OR pass parseAddress("[email protected]") to get { localPart, domain }. | | Pasting Outlook-style ; -separated recipients | Now works — to/cc/bcc accept comma, semicolon, OR newline. | inbox.send({ to: "[email protected]; [email protected]\[email protected]" }) ✓ | | Typing kind: "magic-link" (with a dash) | Now throws locally with the valid options listed. | Use kind: "magic_link" (underscore). | | Passing a non-UUID to getMessage / runFlow / getFlowRun | Now throws locally with a clear "not a UUID" error. | Pass the 36-char UUID from the message/flow/run object. | | Logging the client (e.g. for debugging) leaking the API key | console.log(client) masks the key by default. | Nothing — already handled. | | client.send() from a non-Pro plan | Throws AssertKitError with code: "upgrade_required" and an upgrade-page hint. | Branch on e.code to handle gracefully. | | Test suite hangs forever on a stuck flow | runFlow({ waitForCompletion: true, timeoutMs }) throws after the wall-clock timeout AND after 600 poll iterations as belt-and-braces. | Either is fine; tighten timeoutMs for faster failure. | | Tests leak pending requests past their teardown | Call client.close() in your test teardown; subsequent ops throw AssertKitError("client_closed") so leaks surface as real failures. | Add afterAll(() => client.close()). | | ASSERTKIT_API_KEY unset, silently falling back to anonymous | AssertKit.fromEnv() throws if the env var isn't set. | Use AssertKit.fromEnv() in test setup instead of new AssertKit(). |

Helper exports

import { parseAddress, uniqueLocalPart } from "@assertkit/node";

parseAddress("[email protected]");
// → { localPart: "support", domain: "your.com" }

uniqueLocalPart("signup");
// → "signup-3f8a2b1d4c5e6f70" (64-bit random suffix)

License

MIT