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. The message field reflects the deliverability assessment:
| Deliverability | message | valid |
|---|---|---|
DELIVERABLE | "Valid email address" | true |
RISKY | "Email address is valid but has risk factors" | true |
UNDELIVERABLE | "Email address is unlikely to be deliverable" | false |
UNKNOWN | "Unable to fully verify email deliverability" | varies |
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: undeliverable email
{
"valid": false,
"message": "Email address is unlikely to be deliverable",
"normalizedValue": "user@gmal.com",
"originalValue": "user@gmal.com",
"validationLevel": "PREMIUM",
"emailDetails": {
"signals": {
"syntax_valid": true,
"mx_valid": false,
"typo_detected": true,
"suggested_domain": "gmail.com",
...
},
"risk_score": 95,
"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.
Error response format
When the API cannot process your request (missing input, authentication failure, quota exceeded, etc.), it returns an error with the appropriate HTTP status code:
{
"error": {
"code": "missing_required_field",
"message": "The required field 'value' is missing from the request body.",
"status": 400
}
}| Field | Type | Description |
|---|---|---|
error.code | string | Machine-readable error identifier |
error.message | string | Human-readable description |
error.status | integer | HTTP status code |
HTTP status codes
| Status | Meaning |
|---|---|
200 OK | Validation processed successfully — check the response body for the result |
400 Bad Request | Missing or malformed input (e.g., missing value field) |
401 Unauthorized | Missing or invalid API key |
403 Forbidden | Your plan does not have access to this feature |
429 Too Many Requests | Rate limit exceeded — e.g., FREE plan quota exceeded or too many concurrent / in-flight requests |
500 Internal Server Error | VerifNow server error (rare) |
503 Service Unavailable | Temporary outage — retry after a moment |
Error codes reference
Authentication errors
| Code | Status | Description |
|---|---|---|
missing_api_key | 401 | No X-API-KEY header was provided |
invalid_api_key | 401 | The API key is malformed or does not exist |
revoked_api_key | 401 | The API key has been revoked |
insufficient_permissions | 403 | This key lacks access to the endpoint |
Request errors
| Code | Status | Description |
|---|---|---|
missing_required_field | 400 | The required value field is missing from the request body |
invalid_request | 400 | The request body is malformed or cannot be parsed |
Quota errors
| Code | Status | Description |
|---|---|---|
quota_exceeded | 429 | You have used your quota for this billing period |
FREE plan: requests are blocked when the quota is reached. Paid plans (STARTER, GROWTH, PRO): requests above the quota are allowed but billed per unit at the end of the billing period.
Handling responses in code
JavaScript / TypeScript
interface ValidationResult {
valid: boolean
message: string
normalizedValue: string
originalValue: string
validationLevel: string
emailDetails: {
signals: Record<string, unknown>
risk_score: number
risk_level: string
deliverability: 'DELIVERABLE' | 'RISKY' | 'UNDELIVERABLE' | 'UNKNOWN'
applied_level: string
}
}
interface VerifNowError {
error: {
code: string
message: string
status: number
}
}
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 }),
})
const data = await response.json()
// Handle API errors (4xx / 5xx)
if (!response.ok) {
const err = data as VerifNowError
switch (err.error.code) {
case 'missing_api_key':
case 'invalid_api_key':
throw new Error('Invalid API key. Check your VERIFNOW_API_KEY environment variable.')
case 'quota_exceeded':
throw new Error('Quota exceeded. Upgrade your plan at app.verifnow.io')
default:
throw new Error(`VerifNow error: ${err.error.message}`)
}
}
// Validation processed — check the result
const result = data as ValidationResult
switch (result.emailDetails.deliverability) {
case 'DELIVERABLE':
console.log('✅ Email is deliverable')
break
case 'RISKY':
console.warn('⚠️ Email is valid but has risk factors')
break
case 'UNDELIVERABLE':
console.error('❌ Email is unlikely to be deliverable')
break
case 'UNKNOWN':
console.warn('❓ Unable to fully verify deliverability')
break
}
return result
}Retry with exponential backoff
For 429 and 5xx errors, implement retry logic with backoff:
async function validateEmailWithRetry(
email: string,
maxRetries = 3
): Promise<unknown> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
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 }),
})
if (response.status === 429 || response.status >= 500) {
const retryAfter = response.headers.get('Retry-After')
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.pow(2, attempt) * 1000 // exponential backoff
if (attempt < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delay))
continue
}
}
return await response.json()
} catch (err) {
if (attempt === maxRetries - 1) throw err
}
}
}Best practices
- A
200does not mean the email is valid — always checkvalidanddeliverabilityin the response body - Only non-
200responses indicate API errors — useresponse.okor the status code to distinguish - Log error codes (not just messages) for debugging — codes are stable across versions
- Implement retry logic for
429and5xxerrors - Surface
suggested_domainfromemailDetails.signalsto users when a typo is detected - Monitor your quota in the dashboard to avoid hitting limits