A good deliverability dashboard tells you something has gone wrong before your Monday-morning newsletter goes out to 50,000 people. This article builds exactly that: a small Node.js monitor that fires a placement test on a schedule, waits for the result over Server-Sent Events, and pings Slack if the inbox rate dropped below a threshold. Thirty minutes end to end.
A single monitor.js file under 40 lines that (1) triggers a placement test on every sender you care about, (2) stores historical results in SQLite, (3) posts a Slack alert when today's inbox rate drops below 80%. Scheduled with node-cron, run with node monitor.js.
What we are building
One scheduled process, three endpoints, one outbound webhook. Every night at 02:00 local time the cron job iterates over your list of sender domains. For each sender, it starts a placement test via POST /api/check, subscribes to GET /api/check/{id}/stream over SSE, waits for the final verdict, stores the result in SQLite, and — if inbox rate is under 80% — posts a summary to a Slack webhook. No frontend, no framework, just a single Node process.
Get an API key
Sign in to your Inbox Check account, open Settings → API keys, and generate one. The key starts with ic_live_. Keep it out of git. This walkthrough assumes the key lives in an environment variable called INBOX_CHECK_API_KEY.
Authentication header
Every request uses a Bearer token:
Authorization: Bearer ic_live_xxxxxxxxxxxx
Content-Type: application/jsonStart a test: POST /api/check
The smallest request body starts a test for one sender:
const res = await fetch('https://check.live-direct-marketing.online/api/check', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.INBOX_CHECK_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
senderDomain: 'news.yourbrand.com',
subject: 'Weekly digest',
html: '<html><body>Hello from the monitor.</body></html>',
}),
});
const { id } = await res.json();The response contains a test id. The test itself runs asynchronously — the API fans the message out to the seed mailboxes and records where each one ended up.
Polling GET vs subscribing to SSE
Two ways to fetch results. Pick one depending on how realtime you need the answer.
GET /api/check/{id} (polling)
Cheap, simple, fine for batch monitors. Call every 15 seconds until status === 'complete'.
GET /api/check/{id}/stream (SSE)
A push stream that emits one SSE event per seed-mailbox landing, then a final complete event with the aggregate verdict. Better UX, a single connection, no polling jitter.
import EventSource from 'eventsource';
function waitForResult(id) {
return new Promise((resolve, reject) => {
const url = `https://check.live-direct-marketing.online/api/check/${id}/stream`;
const es = new EventSource(url, {
headers: { Authorization: `Bearer ${process.env.INBOX_CHECK_API_KEY}` },
});
es.addEventListener('complete', (e) => {
es.close();
resolve(JSON.parse(e.data));
});
es.addEventListener('error', (e) => {
es.close();
reject(e);
});
});
}Parse the result payload
The complete event carries a JSON object shaped like:
{
"id": "t_01H9X...",
"status": "complete",
"summary": {
"inboxCount": 18,
"spamCount": 3,
"missingCount": 1,
"total": 22
},
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass" },
"spamAssassinScore": 1.2,
"providers": [ /* per-seed detail */ ]
}Inbox rate is simply summary.inboxCount / summary.total. Anything below 80% is your alert threshold; anything below 50% is on-fire.
Schedule with node-cron
Install the dependency and wire the schedule:
npm install node-cron eventsource better-sqlite3import cron from 'node-cron';
const SENDERS = [
'news.yourbrand.com',
'tx.yourbrand.com',
'ops.yourotherbrand.com',
];
// Every day at 02:00
cron.schedule('0 2 * * *', async () => {
for (const domain of SENDERS) {
try {
await monitorOne(domain);
} catch (err) {
console.error(`Failed ${domain}`, err);
}
}
});Store history
A one-line SQLite schema covers every question you will want to ask later: "is inbox rate trending down?", "which sender is degrading the fastest?".
import Database from 'better-sqlite3';
const db = new Database('monitor.db');
db.exec(`
CREATE TABLE IF NOT EXISTS placements (
id TEXT PRIMARY KEY,
domain TEXT NOT NULL,
ts INTEGER NOT NULL,
inbox INTEGER,
spam INTEGER,
missing INTEGER,
total INTEGER
);
`);
const insert = db.prepare(
'INSERT INTO placements VALUES (?, ?, ?, ?, ?, ?, ?)'
);Postgres would look the same, swap the driver. SQLite is enough for a single-process monitor.
Post to Slack on threshold
Create an incoming webhook in Slack (Apps → Incoming Webhooks), copy the URL into SLACK_WEBHOOK, and:
async function alertSlack(domain, summary) {
const rate = (summary.inboxCount / summary.total * 100).toFixed(1);
const text =
`:warning: *${domain}* inbox rate dropped to *${rate}%*\n` +
`Inbox: ${summary.inboxCount} · Spam: ${summary.spamCount} · Missing: ${summary.missingCount}`;
await fetch(process.env.SLACK_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
}Rate limits and good citizenship
The API allows about 5 tests per minute per key on the standard plan. A nightly monitor over 50 senders takes ten minutes end to end at that rate — fine. If you want higher throughput, batch less frequently instead of pressing the limit.
- Don't hot-loop tests every few minutes. Once a day per sender is usually enough; anything tighter is a waste of your quota.
- Cache your seed list.
list_providersrarely changes. - Respect 429s. Back off exponentially; the server header tells you how long to wait.
A single test is a sample of 20+ mailboxes. That sample is representative but noisy. For serious monitoring, run two tests per sender per night and average the inbox rate — it smooths out the 5–10% random variance you would otherwise see on identical content.
Full Node.js monitor, under 40 lines
import cron from 'node-cron';
import Database from 'better-sqlite3';
import EventSource from 'eventsource';
const KEY = process.env.INBOX_CHECK_API_KEY;
const SLACK = process.env.SLACK_WEBHOOK;
const SENDERS = ['news.yourbrand.com', 'tx.yourbrand.com'];
const BASE = 'https://check.live-direct-marketing.online';
const db = new Database('monitor.db');
db.exec('CREATE TABLE IF NOT EXISTS p (id TEXT, d TEXT, ts INT, i INT, s INT, m INT, t INT)');
async function runOne(domain) {
const r = await fetch(`${BASE}/api/check`, {
method: 'POST',
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ senderDomain: domain, subject: 'Monitor', html: '<p>ok</p>' }),
}).then((x) => x.json());
const res = await new Promise((ok, bad) => {
const es = new EventSource(`${BASE}/api/check/${r.id}/stream`,
{ headers: { Authorization: `Bearer ${KEY}` } });
es.addEventListener('complete', (e) => { es.close(); ok(JSON.parse(e.data)); });
es.addEventListener('error', (e) => { es.close(); bad(e); });
});
const s = res.summary;
db.prepare('INSERT INTO p VALUES (?,?,?,?,?,?,?)')
.run(res.id, domain, Date.now(), s.inboxCount, s.spamCount, s.missingCount, s.total);
if (s.inboxCount / s.total < 0.80) {
await fetch(SLACK, { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: `${domain}: ${s.inboxCount}/${s.total} inbox` }) });
}
}
cron.schedule('0 2 * * *', async () => { for (const d of SENDERS) await runOne(d); });That is the full monitor. Save it as monitor.js, set your two env variables, run node monitor.js under pm2 or systemd, and you will get a Slack ping the next time your Gmail placement falls off.