SDK8 min read

Node.js SDK quick start — first test in 10 minutes

npm install inbox-check, new InboxCheck({ apiKey }), and you’re running placement tests in TypeScript. Here is the full quick start with types, streaming, and webhooks.

The Inbox Check Node.js SDK gives TypeScript-first access to the placement API: typed models for every request and response, an async iterable for streaming results, a webhook signature helper, and drop-in handlers for Express and Next.js route handlers. Ten minutes from an empty project to a running placement test.

What you need

Node.js 18+ (for the built-in fetch and ReadableStream), and an Inbox Check API key. TypeScript is optional but recommended — the SDK ships its own .d.ts and works out of the box with any reasonable tsconfig.

npm install

npm install inbox-check

# or with pnpm:
pnpm add inbox-check

# or yarn:
yarn add inbox-check

The package is published as inbox-check. Dual ESM/CJS build — use import in ESM projects and require in CJS; both work.

TypeScript setup

Any reasonable tsconfig.json works. The SDK is happy with moduleResolution set to bundler, node, or nodenext. If you are on a fresh project:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "lib": ["ES2022", "DOM"]
  }
}

Client initialisation

Put the key in .env and read it from process.env:

// client.ts
import { InboxCheck } from 'inbox-check';

export const ic = new InboxCheck({
  apiKey: process.env.INBOX_CHECK_API_KEY!,
  // optional:
  baseUrl: 'https://check.live-direct-marketing.online',
  timeout: 30_000,           // ms
  maxRetries: 3,             // retry on 5xx
});

First placement test

Three fields required — sender domain, subject, HTML body. Everything else is optional. The response is fully typed:

import { ic } from './client';

const test = await ic.tests.create({
  senderDomain: 'news.acme.io',
  subject: 'Weekly digest',
  html: '<html><body><p>Hello from the monitor.</p></body></html>',
  text: 'Hello from the monitor.',        // plain-text fallback
  from: 'hello@news.acme.io',             // optional
});

console.log(test.id);           // "t_01H9X..."
console.log(test.status);       // "queued"

Polling with types

The built-in helper polls until the test completes and returns the typed result. No manual loop, no setTimeout bookkeeping.

const result = await ic.tests.wait(test.id, {
  intervalMs: 3_000,            // poll every 3s (default)
  timeoutMs: 5 * 60_000,        // 5 minutes (default)
});

// result is typed as CompleteTestResult
console.log(result.summary.inboxRate);       // number in [0, 1]
console.log(result.summary.inboxCount, '/', result.summary.total);
for (const p of result.providers) {
  console.log(`${p.name}: ${p.folder}`);
}

Streaming with AsyncIterable

If you want per-seed events as they arrive — a nice UX for a CLI or a dashboard — use the stream iterator. Under the hood it consumes the server's SSE feed; you get a regular for await ... of loop with discriminated events.

for await (const event of ic.tests.stream(test.id)) {
  switch (event.type) {
    case 'placement': {
      console.log(`[${event.data.name}] ${event.data.folder}`);
      break;
    }
    case 'complete': {
      const s = event.data.summary;
      console.log(`Done: ${(s.inboxRate * 100).toFixed(1)}% inbox`);
      return;
    }
    case 'error': {
      throw new Error(event.data.message);
    }
  }
}
Always close the stream

Breaking out of the for await loop automatically releases the underlying connection. If you call ic.tests.stream(id) and never iterate, you leak the socket. Always either iterate it or call .return() on the returned iterator.

Webhook helper

When you register a webhook URL in the dashboard, Inbox Check POSTs a signed payload on every test completion. Verifying the signature in your handler takes one line:

import { verifyWebhook } from 'inbox-check/webhooks';

// In an Express or Next.js route handler:
const ok = verifyWebhook({
  payload: rawBody,                                  // raw Buffer / string
  signature: req.headers['inbox-check-signature']!,  // hex digest
  secret: process.env.INBOX_CHECK_WEBHOOK_SECRET!,
  toleranceSeconds: 300,                             // default 300
});

if (!ok) {
  return res.status(401).end('bad signature');
}

const event = JSON.parse(rawBody);
if (event.type === 'test.completed') {
  console.log(event.data.summary.inboxRate);
}

Common error classes

Every failure maps to a typed exception so your catch blocks are informative:

import {
  AuthError,            // 401
  RateLimitError,       // 429 (exposes .retryAfter)
  ValidationError,      // 400
  NotFoundError,        // 404
  ServerError,          // 5xx after retries
  InboxCheckError,      // base class
} from 'inbox-check';

try {
  const test = await ic.tests.create({ /* ... */ });
  const result = await ic.tests.wait(test.id);
} catch (err) {
  if (err instanceof RateLimitError) {
    console.warn(`Backoff ${err.retryAfter}s`);
  } else if (err instanceof AuthError) {
    console.error('Bad or revoked API key');
  } else if (err instanceof ValidationError) {
    console.error(`Invalid payload: ${err.message}`);
  } else if (err instanceof InboxCheckError) {
    console.error('Other SDK error', err);
  } else {
    throw err;
  }
}

Integrating with Express / Next.js API routes

A typical pattern: a dashboard endpoint that returns the latest placement for one sender. The SDK hides the HTTP plumbing so your handler stays small.

// app/api/placement/route.ts  (Next.js 14 App Router)
import { NextResponse } from 'next/server';
import { InboxCheck } from 'inbox-check';

const ic = new InboxCheck({ apiKey: process.env.INBOX_CHECK_API_KEY! });

export async function GET(req: Request) {
  const url = new URL(req.url);
  const domain = url.searchParams.get('domain');
  if (!domain) return NextResponse.json({ error: 'domain required' }, { status: 400 });

  const { items } = await ic.tests.list({ senderDomain: domain, limit: 1 });
  const latest = items[0];
  if (!latest) return NextResponse.json({ placement: null });

  return NextResponse.json({
    placement: {
      at: latest.createdAt,
      rate: latest.summary.inboxRate,
      spf: latest.auth.spf,
      dkim: latest.auth.dkim,
      dmarc: latest.auth.dmarc,
    },
  });
}

In Express, the same body inside an app.get('/api/placement', ...) handler works unchanged (minus the NextResponse wrapper).

A complete 50-line dashboard endpoint

Aggregates 7 days of placement per sender, returns a single JSON suitable for a chart. Paste this into a route handler and you have the backend for a mini-dashboard.

// app/api/dashboard/route.ts
import { NextResponse } from 'next/server';
import { InboxCheck } from 'inbox-check';

const ic = new InboxCheck({ apiKey: process.env.INBOX_CHECK_API_KEY! });

export async function GET() {
  const since = new Date(Date.now() - 7 * 86400_000).toISOString();
  const { items } = await ic.tests.list({ since, limit: 200 });

  const perDomain = new Map<string, {
    domain: string;
    tests: number;
    inbox: number;
    total: number;
    rate: number;
    perProvider: Record<string, { inbox: number; spam: number; missing: number }>;
  }>();

  for (const t of items) {
    const entry = perDomain.get(t.senderDomain) ?? {
      domain: t.senderDomain,
      tests: 0, inbox: 0, total: 0, rate: 0, perProvider: {},
    };
    entry.tests += 1;
    entry.inbox += t.summary.inboxCount;
    entry.total += t.summary.total;
    for (const p of t.providers) {
      entry.perProvider[p.name] ??= { inbox: 0, spam: 0, missing: 0 };
      entry.perProvider[p.name][p.folder] += 1;
    }
    perDomain.set(t.senderDomain, entry);
  }

  const rows = Array.from(perDomain.values()).map((r) => ({
    ...r,
    rate: r.total === 0 ? 0 : +(r.inbox / r.total).toFixed(3),
  }));

  return NextResponse.json({
    window: '7d',
    senders: rows.sort((a, b) => a.rate - b.rate),
  });
}

Frequently asked questions

Does the SDK work in Cloudflare Workers / Vercel Edge / Deno?

Yes — it uses fetch and standard web streams, no Node-specific APIs. Import it from either the ESM entry (for Edge) or the CJS entry (for classic Node). The webhook helper uses the WebCrypto API and runs in every runtime that ships one.

Is it tree-shakeable?

Yes. ESM build with /* @__PURE__ */ annotations on factory functions. Importing just verifyWebhook from inbox-check/webhooks does not drag in the rest of the SDK.

How do I retry a failed test without duplicating logic?

Pass a maxRetries option to the client (default 3), or wrap the call yourself. The SDK retries idempotent calls (POST /tests is idempotent by design via the Idempotency-Key header, which the SDK sets automatically).

Can I use it from a browser?

Technically yes, but don't. Exposing an API key client-side leaks it. Always put the SDK behind a route handler on your server and have the browser talk to that.
Related reading

Check your deliverability across 20+ providers

Gmail, Outlook, Yahoo, Mail.ru, Yandex, GMX, ProtonMail and more. Real inbox screenshots, SPF/DKIM/DMARC, spam engine verdicts. Free, no signup.

Run Free Test →

Unlimited tests · 20+ seed mailboxes · Live results · No account required