Node.js Examples
VerifNow provides an official Node.js SDK, @verifnow/sdk. You can also call the API directly
with fetch if you would rather not add a dependency.
Official SDK: 📦 @verifnow/sdk on npm — typed client for all seven validators 💻 verifnowio/verifnow-node — source, changelog and issues
Option 1: Using the official SDK (recommended)
The SDK has zero runtime dependencies, ships ESM and CommonJS with types, and runs on Node 18+.
What it does for you that a hand-written fetch does not:
- typed results —
emailDetails,vatDetails,ibanDetails,phoneDetails, mapped from the API’ssnake_casetocamelCase; - typed errors, so you can tell a bad key from a spent quota from an outage without parsing strings;
- retries on connection failures and 5xx, with backoff — never on a 400 or 401, which would fail the same way twice;
- a timeout on every call (5 seconds by default).
Install and configure
npm install @verifnow/sdkimport { VerifNow } from '@verifnow/sdk';
const client = new VerifNow({
apiKey: process.env.VERIFNOW_API_KEY!,
// timeoutMs: 5000, // default
// retry: { attempts: 2 }, // default; `false` disables retries
});Validate an email
const result = await client.validateEmail('user@gmial.com');
if (!result.valid) {
// A hard fact: malformed, no mail records, or a blocked domain. The message says which.
console.log(result.message);
}
const signals = result.emailDetails?.signals;
if (signals?.typoDetected) {
console.log(`Did you mean user@${signals.suggestedDomain}?`); // gmail.com
}
console.log(signals?.disposable); // true for a throwaway inbox
console.log(signals?.roleBased); // true for contact@, info@ — a signal, not a rejection
console.log(signals?.freeProvider); // undefined on FREE and STARTER: not computed at that depthSignals are reported, not enforced: valid rests on hard facts only, and is the same on every plan.
See Validate Email for which signals each plan returns.
Validate a VAT number
VAT is the one validator where the answer can be unknown rather than yes or no, because it depends on VIES — a registry that goes down. Handle that third case explicitly.
const { vatDetails } = await client.validateVat('IE6388047V');
const vat = vatDetails!;
console.log(vat.formatValid); // true — decided locally, never depends on VIES
console.log(vat.registered); // true | false | null — null means VIES could not be asked
console.log(vat.source); // 'LIVE' | 'CACHE' | 'STALE' | 'UNVERIFIED' | 'NOT_APPLICABLE'
console.log(vat.traderName); // 'GOOGLE IRELAND LIMITED', when the member state discloses it
console.log(vat.checkedAt); // a Date
if (vat.registered === null) {
// Not a rejection: accept, record that it is unconfirmed, and re-check later.
}See Validate VAT for the full degradation contract.
Validate an IBAN
const result = await client.validateIban('FR23111111111111111111111');
console.log(result.valid); // false
console.log(result.message); // 'A FR IBAN is 27 characters long'
console.log(result.ibanDetails?.checksumValid); // true — the check digits are fine…
console.log(result.ibanDetails?.structureValid); // false — …the length is impossible for FranceOn a valid IBAN, store normalizedValue and show ibanDetails.formatted back to the user. See
Validate IBAN.
Validate a phone number
const result = await client.validatePhone('+33 6 12 34 56 78');
console.log(result.normalizedValue); // '+33612345678' — E.164, the form to store
console.log(result.phoneDetails?.countryCode); // 'FR'
console.log(result.phoneDetails?.lineType); // 'MOBILE'The number must include its country code. See Validate Phone.
Handle errors
The SDK throws instead of returning valid: true when a call fails, so that accepting unverified
input is a decision you make rather than an accident.
import {
VerifNow,
VerifNowAuthError,
VerifNowConnectionError,
VerifNowRateLimitError,
VerifNowRequestError,
} from '@verifnow/sdk';
const client = new VerifNow({ apiKey: process.env.VERIFNOW_API_KEY! });
export async function isEmailAcceptable(email: string): Promise<boolean> {
try {
const result = await client.validateEmail(email);
return result.valid && !result.emailDetails?.signals?.disposable;
} catch (error) {
if (error instanceof VerifNowAuthError) {
// A missing, invalid or revoked key. Retrying will not help: fix the configuration.
throw error;
}
if (error instanceof VerifNowRateLimitError) {
// Monthly quota spent, or too many requests in flight. The SDK already retried the ones
// worth retrying; this one is not going to clear in the next second.
throw error;
}
if (error instanceof VerifNowRequestError) {
// The request itself was malformed (400).
throw error;
}
if (error instanceof VerifNowConnectionError) {
// Unreachable or timed out, after retries. Accepting the address unverified is a
// reasonable choice — make it deliberately, and log it.
console.warn('VerifNow unreachable, accepting unverified', { timedOut: error.timedOut });
return true;
}
throw error;
}
}An error the API returned carries its HTTP status and the requestId — quote the request id if
you contact support. A connection error has neither: it never reached the API.
In an Express route
import express from 'express';
import { VerifNow, VerifNowConnectionError } from '@verifnow/sdk';
const client = new VerifNow({ apiKey: process.env.VERIFNOW_API_KEY! });
const router = express.Router();
router.post('/register', async (req, res, next) => {
const { email } = req.body as { email?: string };
if (!email) {
return res.status(400).json({ error: 'Email is required.' });
}
try {
const result = await client.validateEmail(email);
const signals = result.emailDetails?.signals;
if (signals?.typoDetected) {
return res.status(400).json({
error: 'Please check your email address.',
suggestion: `${email.split('@')[0]}@${signals.suggestedDomain}`,
});
}
if (!result.valid) {
return res.status(400).json({ error: result.message });
}
if (signals?.disposable) {
return res.status(400).json({ error: 'Please use a permanent email address.' });
}
} catch (error) {
if (!(error instanceof VerifNowConnectionError)) {
// A bad key or a spent quota is your problem, not the user's: do not show it to them as
// "invalid email", and do not swallow it either. next(error), not throw — Express 4 does
// not catch an exception raised inside an async handler, and the request would hang.
return next(error);
}
// Unreachable after retries: this example lets registration through, unverified.
}
// …create the user
return res.status(201).json({ ok: true });
});
export default router;In a Next.js route handler
// app/api/validate-email/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { VerifNow, VerifNowError } from '@verifnow/sdk';
const client = new VerifNow({ apiKey: process.env.VERIFNOW_API_KEY! });
export async function POST(req: NextRequest) {
const { value } = (await req.json()) as { value?: unknown };
if (typeof value !== 'string' || value === '') {
return NextResponse.json({ error: 'value is required' }, { status: 400 });
}
try {
const result = await client.validateEmail(value);
// Return what your UI needs — not the raw response, and never your API key's errors.
return NextResponse.json({
valid: result.valid,
message: result.message,
suggestion: result.emailDetails?.signals?.suggestedDomain,
});
} catch (error) {
const status = error instanceof VerifNowError ? error.status : undefined;
console.error('VerifNow call failed', { status });
return NextResponse.json({ error: 'Validation is unavailable right now.' }, { status: 503 });
}
}Keep the API key on the server. A route like this is how a browser form gets a verdict without ever seeing the key.
These snippets need @verifnow/sdk 1.3.0 or later. The SDK README lists every method and option:
verifnowio/verifnow-node .
Option 2: Direct API call with fetch
If you would rather not add a dependency, every endpoint is a single POST. You then own what the SDK
does for you: typing, retries, timeouts, and telling the error cases apart.
const response = await fetch('https://api.verifnow.io/api/v1/validate/email', {
method: 'POST',
headers: {
'X-API-KEY': process.env.VERIFNOW_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({ value: 'user@example.com' }),
signal: AbortSignal.timeout(5000),
});
if (!response.ok) {
// Errors come back as {"error": "..."} or {"status": ..., "message": "..."}, depending on where
// they were raised. Read both, and branch on the HTTP status rather than on the text.
const body = (await response.json().catch(() => ({}))) as { error?: string; message?: string };
throw new Error(`VerifNow ${response.status}: ${body.message ?? body.error ?? response.statusText}`);
}
const result = (await response.json()) as {
valid: boolean;
message?: string;
emailDetails?: { signals?: { typo_detected?: boolean; suggested_domain?: string } };
};Note the snake_case inside the details objects: the raw API does not map field names for you.
Check response.ok before reading valid. An error body has no valid field, so code that
reads !body.valid on a 401 or a 429 will tell your user their email is invalid when the real
problem is your API key or your quota.
Validating several values
There is no batch endpoint: send one request per value. For the fields of a single form, send them in parallel — see Validating a B2B signup form for a complete example and Rate Limits for how many requests each plan allows in flight.