SDK9 min read

Node.js SDK — full reference

Constructor options, every method signature, TypeScript types, error classes, retry strategy, webhook helpers. The complete Node.js SDK reference.

This is the exhaustive reference for the official inbox-check package for Node.js. Every constructor option, every method, every TypeScript type, every error class. For a 60-second intro, read inbox-placement-api-nodejs-sdk-quickstart. For the full surface, keep going.

Package details

Name: inbox-check. Runtime: Node.js 18.17+ and 20+, also Bun 1.1+ and Deno 1.40+ via the npm: specifier. ESM-first with CJS interop shim. Zero runtime deps on Node 18+ (uses the built-in fetch). Full TypeScript types shipped.

Install

npm install inbox-check
# pnpm
pnpm add inbox-check
# yarn
yarn add inbox-check
# bun
bun add inbox-check

new InboxCheck()

The client is a single class. One instance, reused everywhere in your process. Safe to share across workers in the same cluster node as long as each worker holds its own reference.

import { InboxCheck } from 'inbox-check';

const client = new InboxCheck({
  apiKey: string;                      // required, or env INBOX_CHECK_API_KEY
  baseURL?: string;                    // default: https://check.live-direct-marketing.online
  timeout?: number;                    // ms, default 30000
  retries?: number;                    // default 3
  retryBackoffMs?: number;             // default 500, exponential
  retryOnStatus?: number[];            // default [429,500,502,503,504]
  userAgent?: string;                  // default "inbox-check-node/1.x.x"
  fetch?: typeof globalThis.fetch;     // inject your own (undici, node-fetch)
  onRequest?: (req: Request) => void;
  onResponse?: (res: Response) => void;
});

tests.create() — full TS signature

interface CreateTestInput {
  senderDomain: string;
  subject: string;
  html?: string;
  text?: string;
  headers?: Record<string, string>;
  fromName?: string;
  providers?: string[];           // subset of seed list
  replyTo?: string;
  tags?: string[];
  webhookUrl?: string;
  idempotencyKey?: string;
  timeout?: number;               // override client default
  signal?: AbortSignal;           // cancellation
}

interface Test {
  id: string;
  status: 'pending' | 'running' | 'complete' | 'failed';
  createdAt: string;              // ISO-8601
  completedAt?: string;
  summary?: Summary;
  auth?: Auth;
  providers: ProviderResult[];
  tags: string[];
}

client.tests.create(input: CreateTestInput): Promise<Test>;

At least one of html or text is required. Pass both to ship a multipart message. The signal is a normal AbortSignal, so you can wire it up to your own cancellation token or an HTTP request's abort.

Example: create a tagged test

const test = await client.tests.create({
  senderDomain: 'news.yourbrand.com',
  subject: 'Weekly digest',
  html: renderTemplate('digest.html'),
  tags: ['digest', `campaign:${campaignId}`],
  idempotencyKey: `digest-${campaignId}`,
});

console.log(test.id);         // "t_01J9..."
console.log(test.status);     // "pending"

tests.get() — fetch one test

client.tests.get(testId: string): Promise<Test>;

// Poll until complete
let t = await client.tests.get(id);
while (t.status !== 'complete' && t.status !== 'failed') {
  await new Promise(r => setTimeout(r, 5000));
  t = await client.tests.get(id);
}
console.log(t.summary);

tests.stream() — AsyncIterable

The stream yields TestEvent objects, one per seed landing plus a terminal complete. It is a native async iterable — use it with for await.

type TestEvent =
  | { type: 'landing'; data: { provider: string; folder: 'inbox' | 'spam' | 'missing' } }
  | { type: 'auth'; data: Auth }
  | { type: 'progress'; data: { completed: number; total: number } }
  | { type: 'complete'; data: Test }
  | { type: 'error'; data: { message: string; code?: string } };

for await (const ev of client.tests.stream(test.id)) {
  if (ev.type === 'landing') {
    console.log(`${ev.data.provider} -> ${ev.data.folder}`);
  } else if (ev.type === 'complete') {
    const s = ev.data.summary!;
    console.log(`${s.inboxCount}/${s.total} inbox`);
    break;
  }
}

The stream uses the Fetch API's streaming response body and a WHATWG SSE parser. No EventSource dependency, no browser-only code paths.

tests.list() — pagination

interface ListParams {
  limit?: number;                 // default 50, max 200
  cursor?: string;
  status?: 'pending' | 'running' | 'complete' | 'failed';
  tag?: string;
  createdAfter?: Date | string;
  createdBefore?: Date | string;
}

interface TestPage {
  data: Test[];
  nextCursor: string | null;
}

// Manual pagination
const page = await client.tests.list({ tag: 'digest', limit: 100 });

// Iterate everything
for await (const t of client.tests.iterAll({ tag: 'digest' })) {
  console.log(t.id, t.summary?.inboxCount);
}

me.get() — quota and plan

const me = await client.me.get();
console.log(me.plan);                    // "standard"
console.log(me.quota.monthlyLimit);      // 5000
console.log(me.quota.usedThisMonth);     // 412
console.log(me.rateLimit.perMinute);     // 5

webhook.verify()

Verify HMAC signatures on webhook callbacks. Works with Express, Fastify, Hono, Next.js route handlers — anything that hands you the raw body.

import { webhook } from 'inbox-check';

app.post('/webhooks/inbox-check',
  express.raw({ type: 'application/json' }),  // raw body is essential
  (req, res) => {
    try {
      const event = webhook.verify(
        req.body,                           // Buffer
        req.header('x-inboxcheck-signature')!,
        { secret: process.env.WEBHOOK_SECRET! },
      );
      if (event.type === 'test.complete') {
        void enqueueProcess(event.data);
      }
      res.status(200).end();
    } catch (err) {
      res.status(401).end();
    }
  });
Raw body, not parsed JSON

Signature verification runs over the exact bytes that left our server. If your framework parses JSON first and re-serialises, the signature will not match. Always wire up the raw body middleware for the webhook route only.

Error classes

  • InboxCheckError — base class. Has .status, .code, .requestId.
  • AuthError — 401/403.
  • RateLimitError — 429. .retryAfter in seconds.
  • NotFoundError — 404.
  • ValidationError — 422, with .fields.
  • ServerError — 5xx, retried automatically.
  • TimeoutError — client-side timeout.
import { InboxCheck, RateLimitError, ValidationError } from 'inbox-check';

try {
  await client.tests.create({ senderDomain: 'x', subject: '' });
} catch (err) {
  if (err instanceof ValidationError) {
    console.error('bad payload:', err.fields);
  } else if (err instanceof RateLimitError) {
    console.error(`slow down, retry in ${err.retryAfter}s`);
  } else {
    throw err;
  }
}

Retry and timeout semantics

Timeout is per attempt, not per call. A timeout of 30000ms with retries of 3 means up to 4 attempts of 30s each, so 120s total wall clock in the worst case. If you need a hard ceiling, pass an AbortSignal that fires after your maximum.

const ac = new AbortController();
const hardTimeout = setTimeout(() => ac.abort(), 60_000);

try {
  const test = await client.tests.create({
    senderDomain: 'news.yourbrand.com',
    subject: 'Hard timeout demo',
    html: '<p>ok</p>',
    signal: ac.signal,
  });
} finally {
  clearTimeout(hardTimeout);
}

On 429, the SDK reads the Retry-After header and uses that instead of exponential backoff. On 5xx, it backs off exponentially with jitter.

Logging hook

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

const client = new InboxCheck({
  onRequest: (req) => logger.info({ url: req.url, method: req.method }, 'ic.req'),
  onResponse: (res) => logger.info({ url: res.url, status: res.status }, 'ic.res'),
});

Full TypeScript types — at a glance

interface Summary {
  inboxCount: number;
  spamCount: number;
  missingCount: number;
  total: number;
  inboxRate: number;          // 0..1
}

interface Auth {
  spf: 'pass' | 'fail' | 'softfail' | 'none';
  dkim: 'pass' | 'fail' | 'none';
  dmarc: 'pass' | 'fail' | 'none';
  aligned: boolean;
}

interface ProviderResult {
  provider: string;           // "gmail", "outlook", "yahoo", ...
  folder: 'inbox' | 'spam' | 'missing';
  headers?: Record<string, string>;
  spamAssassinScore?: number;
}

Browser usage caveats

The SDK runs in browsers with a caveat: do not ship your API key to a browser. Use a server-side proxy that forwards requests and attaches the key. A public-facing key that can be scraped from view-source will be abused within hours.

Full example: Express webhook receiver

import express from 'express';
import { InboxCheck, webhook } from 'inbox-check';
import { db } from './db';

const app = express();
const client = new InboxCheck();

app.post('/hooks/inbox-check',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const sig = req.header('x-inboxcheck-signature');
    if (!sig) return res.status(400).end();
    let event;
    try {
      event = webhook.verify(req.body, sig, {
        secret: process.env.WEBHOOK_SECRET!,
      });
    } catch {
      return res.status(401).end();
    }

    if (event.type === 'test.complete') {
      const t = event.data;
      await db.placements.insert({
        id: t.id,
        inbox: t.summary!.inboxCount,
        total: t.summary!.total,
        at: new Date(),
      });
      if (t.summary!.inboxRate < 0.8) {
        await slack.alert(t);
      }
    }
    res.status(200).end();
  });

app.listen(3000);

Frequently asked questions

Is the SDK ESM only?

Ships both. The primary export is ESM; CJS users get a synthetic default via the package's exports map. require('inbox-check') works on Node 18.17+.

Does it work on Cloudflare Workers?

Yes. The SDK uses the Fetch API exclusively and has no Node built-ins. Pass env.INBOX_CHECK_API_KEY through the apiKey option. Streams work under Workers too.

How do I mock the SDK in Jest/Vitest?

Inject your own fetch via the fetch option. Both Jest and Vitest can spy on or replace a fetch function you pass in, so you never need to touch the global.

Can I run two clients (staging + prod) in the same process?

Yes. Instantiate two InboxCheck objects with different apiKey and baseURL values. They share no global state.
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