Skip to Content
ExamplesNode.js

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


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’s snake_case to camelCase;
  • 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/sdk
import { 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 depth

Signals 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 France

On 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.

Last updated on