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
Found this useful? Share it
AB
About the author
Artem Berezin
B2B Deliverability Specialist

B2B deliverability specialist with 5+ years of hands-on outreach experience. Built campaigns reaching 90,000+ inboxes across 20+ countries — and fixed the deliverability problems that came with that scale.

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