Skip to Content
GuidesError Handling

Error Handling

The VerifNow API uses standard HTTP status codes and returns consistent response objects so you can handle every scenario gracefully.

Important: When a validation is successfully processed, the API always returns 200 OK — regardless of whether the email is valid or not. Check the valid, message, and deliverability fields in the response body to determine the validation outcome.


Successful validation responses (200 OK)

When the API processes your validation request, it returns 200 OK with a response body containing the result.

For email, valid is false only when the address cannot receive mail. The message says why:

Reasonmessagevalid
Empty input"Email cannot be empty"false
Invalid syntax"Invalid email format"false
Domain has neither MX nor A records"Domain has no valid MX or A records"false
Domain is on the blocklist"Domain reputation is blocked"false

Otherwise valid is true on every plan, and the message reflects the deliverability assessment:

Deliverabilitymessagevalid
DELIVERABLE"Valid email address"true
RISKY"Email address is valid but has risk factors"true
UNDELIVERABLE"Email address is valid but unlikely to be deliverable"true

Risk signals such as a likely typo or a disposable provider lower deliverability, never valid. Decide what to accept from deliverability, risk_score and the signals, as shown in Acceptance strategy.

Example: deliverable email

{ "valid": true, "message": "Valid email address", "normalizedValue": "john@gmail.com", "originalValue": "john@gmail.com", "validationLevel": "PREMIUM", "emailDetails": { "signals": { ... }, "risk_score": 5, "risk_level": "LOW", "deliverability": "DELIVERABLE", "applied_level": "PREMIUM" } }

Example: domain that cannot receive mail

{ "valid": false, "message": "Domain has no valid MX or A records", "normalizedValue": null, "originalValue": "user@gmal.com", "validationLevel": "PREMIUM", "emailDetails": { "signals": { "syntax_valid": true, "mx_valid": false, "typo_detected": true, "suggested_domain": "gmail.com", ... }, "risk_score": 100, "risk_level": "HIGH", "deliverability": "UNDELIVERABLE", "applied_level": "PREMIUM" } }

A 200 response does not mean the email is valid. Always check the valid and deliverability fields in the response body.


When the request itself fails

Anything other than 200 means the API could not process the request. There are two error shapes, because errors are raised in two places:

Raised byShapeExample
The edge (authentication, the FREE quota, unknown paths){"error": "..."}{"error": "Invalid API key"}
The API itself{"status": 400, "message": "..."}{"status": 429, "message": "Too many requests in flight for this account. Retry in a moment."}

Branch on the HTTP status code, not on the text. There are no machine-readable error codes: the message is written for a person and may be reworded. If you log something, log the status and the X-Request-Id header — every response carries one, and it is what support will ask for.


HTTP status codes

StatusWhenWhat to do
200 OKThe validation ranRead valid — see above. Not an error, even when valid is false.
400 Bad RequestThe body is not JSONFix the request. Retrying sends the same broken body.
401 UnauthorizedNo X-API-KEY headerFix the configuration. Do not retry.
403 ForbiddenThe key is invalid or revokedFix the configuration. Do not retry.
404 Not FoundUnknown pathCheck the endpoint — validation paths are /api/v1/validate/{rule}.
405 Method Not AllowedNot a POSTSend a POST.
415 Unsupported Media TypeContent-Type is not application/jsonSet the header.
429 Too Many RequestsFREE quota spent, or too many requests in flightDepends which — read Retry-After. See Rate Limits.
5xxA server or upstream failureRetry with backoff.

A missing or empty value is not an error. {} or {"value": ""} is processed and returns 200 with valid: false and a message such as "Email cannot be empty". Only a body that is not JSON at all returns 400.

What the common errors look like

HTTP/2 401 X-Request-Id: 4f2c… {"error": "Missing API key"}
HTTP/2 403 X-Request-Id: 9a71… {"error": "Invalid API key"}
HTTP/2 400 X-Request-Id: c03e… {"status": 400, "message": "Request body must be JSON, for example {\"value\": \"user@example.com\"}"}

Handling errors in code

With the SDK

The Node.js SDK, @verifnow/sdk, turns these into typed errors, retries what is worth retrying — connection failures, 5xx, and a 429 whose Retry-After is short — and never retries a 400, 401 or 403. The Java SDK does not retry: see its README for how it reports failures.

import { VerifNow, VerifNowAuthError, VerifNowConnectionError, VerifNowRateLimitError, VerifNowRequestError, } from '@verifnow/sdk' const client = new VerifNow({ apiKey: process.env.VERIFNOW_API_KEY! }) export async function validate(email: string) { try { return await client.validateEmail(email) } catch (error) { if (error instanceof VerifNowAuthError) { // 401 / 403 — the key is missing, invalid or revoked. throw new Error('Check the VERIFNOW_API_KEY environment variable.', { cause: error }) } if (error instanceof VerifNowRateLimitError) { // 429 that did not clear within the SDK's retries — usually a spent FREE quota. console.warn('VerifNow quota or concurrency limit', { retryAfterSeconds: error.retryAfterSeconds, requestId: error.requestId, }) throw error } if (error instanceof VerifNowRequestError) { throw error // 400 — the request needs fixing, not retrying } if (error instanceof VerifNowConnectionError) { throw error // unreachable or timed out, after retries — your call whether to accept unverified } throw error } }

See the Node.js examples for the full pattern.

Without the SDK

Check the status before reading valid. An error body has no valid field, so code that reads it on a 401 or a 429 will tell your user their email is invalid when the problem is your key or your quota.

export async function validateEmail(email: string) { 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: email }), signal: AbortSignal.timeout(5000), }) if (!response.ok) { // Either shape: {"error": "..."} from the edge, {"status", "message"} from the API. const body = (await response.json().catch(() => ({}))) as { error?: string; message?: string } const text = body.message ?? body.error ?? response.statusText const requestId = response.headers.get('X-Request-Id') if (response.status === 401 || response.status === 403) { throw new Error(`VerifNow rejected the API key: ${text} (request ${requestId})`) } if (response.status === 429) { const retryAfter = Number(response.headers.get('Retry-After') ?? 'NaN') throw new Error(`VerifNow limit: ${text}` + (Number.isFinite(retryAfter) ? ` — retry in ${retryAfter}s` : '')) } throw new Error(`VerifNow ${response.status}: ${text} (request ${requestId})`) } return (await response.json()) as { valid: boolean; message?: string } }

Retrying

Retry 5xx and connection failures with exponential backoff. Retry a 429 only when Retry-After is short: a concurrency limit clears in a second, a spent monthly quota does not, and sleeping on it helps nobody. Never retry 400, 401 or 403 — they fail the same way every time.


Best practices

  1. A 200 does not mean the value is valid — read valid.
  2. Branch on the status code, not the message — messages are for people and may change.
  3. Log the X-Request-Id with every failure.
  4. Retry 5xx and short 429s only.
  5. Surface suggested_domain from emailDetails.signals when a typo is detected.
  6. Watch your quota with the X-RateLimit-* headers — see Rate Limits.
Last updated on