API rate limits and error handling: every status code explained

Last updated May 19, 2026API

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

EndpointPer-minutePer-day
/verify-single60 requests10,000 requests
/verify-bulk60 requests10,000 requests
/get-results/{task_id}120 requests10,000 requests
Requests, not emails
These limits apply to API requests, not to the number of emails being verified. A bulk task with 50,000 emails counts as one request against the /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
HeaderDescription
X-RateLimit-LimitMax requests allowed in the per-minute window for this endpoint
X-RateLimit-RemainingRequests remaining in the current minute window
X-DailyLimit-RemainingRequests remaining today (UTC)

Rate-limit-exceeded response (429)

Per-minute limit hit

json
{
  "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: 1747615245

Retry-After is the safe wait time in seconds. X-RateLimit-Reset is a Unix timestamp.

Daily limit hit

json
{
  "error": "Daily limit exceeded",
  "limit": 10000,
  "current": 10001,
  "resets_at": "midnight UTC"
}
Repeated rate-limit hits trigger IP protection
Hitting limits occasionally is fine. Hammering an endpoint into a sustained wall of 429s will eventually trigger automatic IP blocking. Back off when you see the first 429.

Error matrix per endpoint

`/verify-single`

HTTPErrorCause
400Email is requiredMissing email field in body
401Missing or invalid Authorization headerNo Bearer token
401Invalid or inactive API keyKey doesn't exist or has been deleted/disabled
402Insufficient creditsLess than 1 credit available
403API key is suspendedKey was suspended by you or by abuse-protection
403Access deniedSource IP is blocked
429Rate limit exceeded60+ requests in the last minute
429Daily limit exceeded10,000+ requests today
500Internal server errorIssue on our side — retry

`/verify-bulk`

HTTPErrorCause
400Emails array is required and must not be emptyMissing or empty emails
400Maximum 1,000,000 emails allowed per requestTask is too large
401Missing or invalid Authorization headerNo Bearer token
401Invalid or inactive API keyKey doesn't exist or has been deleted/disabled
402Insufficient creditsNot enough credits for the task size
403API key is suspendedKey was suspended
403Access deniedSource IP is blocked
429Rate limit exceeded60+ requests in the last minute
429Daily limit exceeded10,000+ requests today
500Internal server errorIssue on our side — retry

`/get-results/{task_id}`

HTTPErrorCause
400Task ID is requiredNo task_id in the URL
401Missing or invalid Authorization headerNo Bearer token
401Invalid or inactive API keyKey doesn't exist or has been deleted/disabled
403Unauthorized to access this taskTask belongs to another account's API key
403API key is suspendedKey was suspended
403Access deniedSource IP is blocked
404Task not foundTask ID doesn't exist or has expired (15-day retention)
429Rate limit exceeded120+ requests in the last minute
429Daily limit exceeded10,000+ requests today
500Internal server errorIssue on our side — retry

Sample error response shapes

Missing Authorization (401)

json
{
  "error": "Missing or invalid Authorization header. Use: Authorization: Bearer VEC..."
}

Invalid key (401)

json
{
  "error": "Invalid or inactive API key"
}
Auth failures are logged
Repeated invalid-key attempts from the same IP eventually trigger automatic IP blocking. Make sure your key reads from a consistent source — a typo in an env var will burn through the threshold quickly.

Suspended key (403)

json
{
  "error": "API key is suspended",
  "reason": "This API key has been suspended"
}

IP blocked (403)

json
{
  "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)

json
{
  "error": "Insufficient credits",
  "current_balance": 0,
  "message": "Please purchase more credits to continue"
}

Insufficient credits — bulk (402)

json
{
  "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.
Bypass attempts make the block permanent
Rotating IPs to dodge a block is the fastest path to a permanent ban. Pause, diagnose, fix.

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

javascript
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

javascript
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

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:

javascript
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-Remaining and slow down proactively.
  • Use the Retry-After header on 429 responses — do not invent a wait time.
  • Implement exponential backoff for 429 and 5xx only.
  • 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_until field 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