API rate limits and error handling: every status code explained
Rate limits keep the platform stable for everyone. This page documents the exact limits per endpoint, every error code you can receive, and code patterns for handling each one without blowing up your integration.
Rate limits
Per-endpoint
| Endpoint | Per-minute | Per-day |
|---|---|---|
/verify-single | 60 requests | 10,000 requests |
/verify-bulk | 60 requests | 10,000 requests |
/get-results/{task_id} | 120 requests | 10,000 requests |
/verify-bulk endpoint.Per-minute uses a rolling 60-second window. Daily resets at midnight UTC.
Rate limit headers (on every successful response)
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-DailyLimit-Remaining: 9500| Header | Description |
|---|---|
X-RateLimit-Limit | Max requests allowed in the per-minute window for this endpoint |
X-RateLimit-Remaining | Requests remaining in the current minute window |
X-DailyLimit-Remaining | Requests remaining today (UTC) |
Rate-limit-exceeded response (429)
Per-minute limit hit
{
"error": "Rate limit exceeded",
"limit": 60,
"window": "1 minute",
"current": 61,
"retry_after_seconds": 45
}Response headers:
Retry-After: 45
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1747615245Retry-After is the safe wait time in seconds. X-RateLimit-Reset is a Unix timestamp.
Daily limit hit
{
"error": "Daily limit exceeded",
"limit": 10000,
"current": 10001,
"resets_at": "midnight UTC"
}Error matrix per endpoint
`/verify-single`
| HTTP | Error | Cause |
|---|---|---|
| 400 | Email is required | Missing email field in body |
| 401 | Missing or invalid Authorization header | No Bearer token |
| 401 | Invalid or inactive API key | Key doesn't exist or has been deleted/disabled |
| 402 | Insufficient credits | Less than 1 credit available |
| 403 | API key is suspended | Key was suspended by you or by abuse-protection |
| 403 | Access denied | Source IP is blocked |
| 429 | Rate limit exceeded | 60+ requests in the last minute |
| 429 | Daily limit exceeded | 10,000+ requests today |
| 500 | Internal server error | Issue on our side — retry |
`/verify-bulk`
| HTTP | Error | Cause |
|---|---|---|
| 400 | Emails array is required and must not be empty | Missing or empty emails |
| 400 | Maximum 1,000,000 emails allowed per request | Task is too large |
| 401 | Missing or invalid Authorization header | No Bearer token |
| 401 | Invalid or inactive API key | Key doesn't exist or has been deleted/disabled |
| 402 | Insufficient credits | Not enough credits for the task size |
| 403 | API key is suspended | Key was suspended |
| 403 | Access denied | Source IP is blocked |
| 429 | Rate limit exceeded | 60+ requests in the last minute |
| 429 | Daily limit exceeded | 10,000+ requests today |
| 500 | Internal server error | Issue on our side — retry |
`/get-results/{task_id}`
| HTTP | Error | Cause |
|---|---|---|
| 400 | Task ID is required | No task_id in the URL |
| 401 | Missing or invalid Authorization header | No Bearer token |
| 401 | Invalid or inactive API key | Key doesn't exist or has been deleted/disabled |
| 403 | Unauthorized to access this task | Task belongs to another account's API key |
| 403 | API key is suspended | Key was suspended |
| 403 | Access denied | Source IP is blocked |
| 404 | Task not found | Task ID doesn't exist or has expired (15-day retention) |
| 429 | Rate limit exceeded | 120+ requests in the last minute |
| 429 | Daily limit exceeded | 10,000+ requests today |
| 500 | Internal server error | Issue on our side — retry |
Sample error response shapes
Missing Authorization (401)
{
"error": "Missing or invalid Authorization header. Use: Authorization: Bearer VEC..."
}Invalid key (401)
{
"error": "Invalid or inactive API key"
}Suspended key (403)
{
"error": "API key is suspended",
"reason": "This API key has been suspended"
}IP blocked (403)
{
"error": "Access denied",
"reason": "IP address is blocked",
"blocked_until": "2026-12-31T23:59:59Z"
}blocked_until is either an ISO timestamp when the block expires, or the string "permanent".
Insufficient credits — single (402)
{
"error": "Insufficient credits",
"current_balance": 0,
"message": "Please purchase more credits to continue"
}Insufficient credits — bulk (402)
{
"error": "Insufficient credits",
"required": 5000,
"current_balance": 1000,
"message": "You need 5000 credits but only have 1000 available"
}IP blocking
Suspicious activity automatically triggers IP-level blocking. Triggers:
- Repeated invalid-API-key attempts.
- Excessive failed authentication on the same IP.
- Attempts to bypass rate limits (sustained 429s with no backoff).
- Unusual or scripted request patterns.
If you think a block is in error, email support@validemailchecker.com with your account email and the affected IP.
Credit usage and refunds
- 4xx errors (bad request, auth, rate limit, etc.) — no credit deducted.
- 5xx errors — no credit deducted.
- `unknown` verification result — credit auto-refunded after task completion.
- Every other definitive result — credit consumed as expected.
See refunds and credit returns for the full refund matrix.
Handling errors in code
Node.js — full switch
const API_KEY = process.env.VEC_API_KEY;
const BASE = 'https://app.validemailchecker.com/api';
async function verifyEmail(email) {
const response = await fetch(`${BASE}/verify-single`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
const data = await response.json();
switch (response.status) {
case 200: return data;
case 400: throw new Error(`Bad request: ${data.error}`);
case 401: throw new Error('Invalid API key — check your credentials');
case 402: throw new Error(`Insufficient credits. Balance: ${data.current_balance}`);
case 403:
if (data.error.includes('suspended')) throw new Error('API key suspended');
if (data.error.includes('blocked')) throw new Error(`IP blocked until: ${data.blocked_until}`);
throw new Error(`Access denied: ${data.error}`);
case 429: {
const retry = data.retry_after_seconds || 60;
throw new Error(`Rate limited — retry after ${retry}s`);
}
case 500: throw new Error('Server error — please retry');
default: throw new Error(`Unexpected error: ${data.error}`);
}
}Node.js — exponential backoff for 429 and 5xx
async function verifyWithRetry(email, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await verifyEmail(email);
} catch (error) {
if (error.message.includes('Rate limited')) {
const waitMs = Math.pow(2, attempt) * 30_000; // 30s, 60s, 120s
await new Promise(r => setTimeout(r, waitMs));
continue;
}
if (error.message.includes('Server error') && attempt < maxRetries - 1) {
await new Promise(r => setTimeout(r, 5_000));
continue;
}
throw error; // never retry 4xx auth/credit errors
}
}
throw new Error('Max retries exceeded');
}Python
import os, time, requests
API_KEY = os.environ['VEC_API_KEY']
BASE = 'https://app.validemailchecker.com/api'
def verify_email(email):
r = requests.post(
f'{BASE}/verify-single',
headers={'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json'},
json={'email': email},
)
data = r.json()
if r.status_code == 200: return data
if r.status_code == 401: raise Exception('Invalid API key')
if r.status_code == 402: raise Exception(f"Insufficient credits: {data.get('current_balance', 0)} remaining")
if r.status_code == 403: raise Exception(f"Access denied: {data.get('error')}")
if r.status_code == 429:
raise Exception(f"Rate limited. Retry after {data.get('retry_after_seconds', 60)}s")
raise Exception(f"API error: {data.get('error')}")
def verify_with_retry(email, max_retries=3):
for attempt in range(max_retries):
try:
return verify_email(email)
except Exception as e:
if 'Rate limited' in str(e):
time.sleep((2 ** attempt) * 30)
continue
raise
raise Exception('Max retries exceeded')Watching the headers proactively
A better pattern than reacting to 429s is reacting to the remaining-count header before you blow through it:
async function makeRequest(endpoint, options) {
const response = await fetch(`${BASE}${endpoint}`, options);
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '60', 10);
const dailyRemaining = parseInt(response.headers.get('X-DailyLimit-Remaining') || '10000', 10);
if (remaining < 10) {
console.warn(`Only ${remaining} requests left this minute — slowing down`);
await new Promise(r => setTimeout(r, 1_000));
}
if (dailyRemaining < 100) {
console.warn(`Only ${dailyRemaining} requests left today — escalate`);
}
return response;
}Best practices
Do
- Read
X-RateLimit-Remainingand slow down proactively. - Use the
Retry-Afterheader on 429 responses — do not invent a wait time. - Implement exponential backoff for
429and5xxonly. - Log every non-200 response with the full error body for debugging.
- Use separate keys per environment (dev, staging, prod) to keep usage tracked separately.
Do not
- Retry authentication errors (401/403) — they will not succeed.
- Ignore rate-limit responses and keep retrying immediately.
- Share one API key across many servers — split them.
- Ignore the
blocked_untilfield on a 403 IP block. - Rotate IPs to bypass blocks (this makes the block permanent).
Common questions
What counts as a request?
Each API call. Even bulk tasks with millions of emails count as one request per call, plus the polls against get-results.
Do failed requests count toward limits?
Yes. All requests count, including 4xx and 5xx responses. That is part of why backoff matters.
Can I get higher limits?
Email support@validemailchecker.com with your use case. Limit raises are evaluated case-by-case.
Why was my IP blocked?
Repeated authentication failures or sustained abuse patterns trigger the block. Most blocks are short-lived; check the blocked_until field. Audit your integration for a key typo or a tight error-retry loop.
Are credits refunded for `unknown` results?
Yes, automatically. Credits are only consumed for verifications that complete with a definitive answer.
Next steps
Related questions
Still stuck? Email support
