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.
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-checknew 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); // 5webhook.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();
}
});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..retryAfterin 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);