Requirements
Node.js 18 or newer, which has fetch built in. Store your key in an environment variable such as VERIDOCK_API_KEY and never send it to the browser.
A client that retries correctly
VeriDock limits concurrent requests per key, not requests per minute. When you go over, it returns 429 with a Retry-After header in seconds. The function below waits that long before retrying, backs off exponentially on 5xx errors, and gives up after a few attempts. Retried requests are idempotent and never double-billed, so retrying is safe.
Other 4xx errors are not retried, because repeating the same request will not fix a bad key or exhausted credits.
const API = "https://app.veridock.io/v1";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export class VeriDockError extends Error {
constructor(status, code, message) {
super(message);
this.status = status;
this.code = code;
}
}
async function request(path, init = {}, attempts = 4) {
for (let attempt = 1; ; attempt++) {
let res;
try {
res = await fetch(API + path, {
...init,
headers: { Authorization: `Bearer ${process.env.VERIDOCK_API_KEY}`, ...init.headers },
signal: AbortSignal.timeout(15_000),
});
} catch (err) {
if (attempt >= attempts) throw err; // network error or timeout
await sleep(2 ** attempt * 250);
continue;
}
if (res.ok) return res.json();
const retryable = res.status === 429 || res.status >= 500;
if (retryable && attempt < attempts) {
const retryAfter = Number(res.headers.get("retry-after"));
await sleep(retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 250);
continue;
}
const body = await res.json().catch(() => ({}));
throw new VeriDockError(res.status, body.error?.code ?? "unknown_error", body.error?.message ?? res.statusText);
}
}
export function verify(email, { smtp = true } = {}) {
const qs = new URLSearchParams({ email, smtp: String(smtp) });
return request(`/verify?${qs}`);
}Handle the errors you can act on
Two errors deserve their own handling. quota_exceeded (402) means your credits and spend cap are used up: alert someone and stop sending traffic until it is raised. unauthorized (401) means the key is wrong or revoked, which is a deployment problem, not a user problem.
import { verify, VeriDockError } from "./veridock.js";
try {
const result = await verify("jane.doe@acme.com");
console.log(result.status, result.reason, result.score);
} catch (err) {
if (err instanceof VeriDockError && err.code === "quota_exceeded") {
notifyOps("VeriDock credits exhausted"); // your alerting
} else {
throw err;
}
}Verify a whole list with a bulk job
For more than a handful of addresses, create one bulk job instead of looping over /verify. Jobs run in the background. Pass a webhook_url to be told when the job finishes, or poll the job until its status is completed.
// Uses request() and sleep() from the client above.
export async function createJob(name, emails, webhookUrl) {
return request("/bulk", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, emails, webhook_url: webhookUrl }),
});
// → { id: "job_8f2k1", status: "queued", total: 3 }
}
export async function waitForJob(id, intervalMs = 5000) {
for (;;) {
const job = await request(`/bulk/${id}`);
if (job.status === "completed") return job;
if (job.status === "failed") throw new Error(`Job ${id} failed: ${job.error}`);
await sleep(intervalMs);
}
}Receive the webhook
When the job finishes, VeriDock POSTs the job object to your webhook URL and signs it with an HMAC-SHA256 of the raw body in the X-VeriDock-Signature header. Compute the signature over the exact bytes you received, before any JSON parsing, and compare in constant time. Failed deliveries are retried with exponential backoff for 24 hours, so return a 2xx quickly and do slow work afterwards.
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/hooks/veridock", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-VeriDock-Signature") ?? "";
const expected = crypto.createHmac("sha256", process.env.VERIDOCK_WEBHOOK_SECRET).update(req.body).digest("hex");
const valid = header.length === expected.length && crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const job = JSON.parse(req.body.toString("utf8"));
res.sendStatus(204);
queueResultsDownload(job.id, job.download_url); // your background work
});Check the lengths before timingSafeEqual: it throws if the two buffers differ in length, which would turn a bad signature into a 500.
Reference
Frequently asked questions
Keep reading
Other guides: Next.js · Python · React · PHP · Supabase · Zapier, Make and n8n