The Verifier
API
Verify one address synchronously, or submit up to a million emails per job. Choose main-pass results or wait for advanced verification to settle. One auth model. Predictable JSON.
Authorization header as a bearer token. Keep API keys server-side and rotate them if they are exposed.Understand the lifecycle first
An email verdict, a completed job, and a final result set are three different things.
There are three different meanings of status: an email classification (valid, invalid, unknown, catch-all, out-of-storage), a main job status (such as processing or completed), and additional_checks.status for tracked follow-up work. Never compare values from one layer with another.
Advanced verification is automatic follow-up work for eligible uncertain addresses, including provider-specific checks and slower SMTP paths. It can improve unknown or catch-all classifications after the main pass. Eligibility and available checks vary; not every uncertain address gets a definitive answer.
track_finality does not turn on every advanced check. It opts a new bulk job into durable tracking and sealing so your integration knows when automatic result changes have ended. Single verification is synchronous and does not return a bulk job ID for these companion endpoints.
Choose fast results when you can tolerate later classification changes: submit normally, wait for completed, then read/export. Choose stable results for a fixed downstream report: submit with track_finality: true, save the ID, wait for results_final === true, then begin pagination or export.
Backward compatibility: track_finality defaults to false. Existing response shapes, main status values, progress, completed_at, completion-email timing, CSV columns and billing semantics stay unchanged. Main progress is not held at 99%; completed and 100% still refer to the main pass. There is no finality webhook or new final-only export endpoint.
Fast results · existing clients
Submit → poll main job → completed → read or export.
Results can still change. No migration required.
Stable results · explicit opt-in
Submit with tracking → poll finality → results_final is true → read or export.
Use for a fixed downstream report. See the complete examples.
Main job states
| status | Meaning | Your client should… |
|---|---|---|
queued | Accepted; waiting for worker capacity. | Keep the job ID and poll. Do not resubmit because progress is zero. |
processing | The main verification pass is running. | Poll every 5–15 seconds. Partial rows and counts can change. |
paused | Work has been paused; this is not success or failure. | Stop automatic waiting and investigate in the dashboard or with support. Resume polling the same ID after work resumes. No public pause/resume API is promised here. |
completed | The main pass finished. CSV export is available. | Use results now if preliminary results are acceptable. For stable results, also require results_final === true. |
error | The main job failed. | Stop waiting; investigate. Partial results are not a complete report. Do not automatically submit a replacement. |
canceled | The job was canceled. | Stop waiting. Retain the ID and decide explicitly whether to start another billable job. |
If you encounter an unfamiliar state, preserve the raw response and job ID and alert your operator. Do not assume success or poll forever. paused is included because an existing or reused job can be paused; this guide does not promise a public pause/resume endpoint.
Advanced verification states
| additional_checks.status | Meaning | Your client should… |
|---|---|---|
waiting | Tracked; the main pass can still discover checks, but none have been registered yet. | Keep polling. Zero pending is not proof of finality. |
running | Tracked follow-up work is active, or its producer is closing. | Keep polling even if the main job is completed or pending is zero. |
finished | The tracked result set has been sealed. | Require results_final === true, inspect failed/expired/stop_reason, then download. |
aborted | Tracking ended without a successfully completed, sealed result set. | Stop waiting. Inspect stop_reason; do not treat this as a final successful report. |
not_tracked | This job has no finality ledger, including reused historical jobs. | Stop finality polling. Either accept legacy results or explicitly create a new normally charged tracked run. |
Why checks stopped
| stop_reason | Meaning |
|---|---|
null | No terminal reason yet, or the job is untracked. Check additional_checks.status. |
completed | All registered checks reached a terminal outcome. Some can have failed or remained inconclusive. |
no_checks | The main pass completed and no follow-up checks were needed. |
deadline | The follow-up time limit was reached; remaining checks expired. A complete main result set is sealed as-is. |
error | The main job failed; finality was aborted. |
canceled | The main job was canceled; finality was aborted. |
incomplete | The run could not prove complete primary processing, for example after a manual completion. Finality was aborted. |
Suggested UI copy for completed plus nonfinal results: “Main verification complete. Advanced verification in progress; results may change.” Show separate warnings for untracked or aborted runs. A finished run with expired or failed checks also deserves an outcome warning.
Quickstart
Verify your first address in 60 seconds.
- Create or copy an API key from the dashboard.
- Send a
POSTrequest tohttps://app.optimaverifier.com/api/v1/verifications/singlewith your key in theAuthorizationheader. - Read
statusfrom the response. It will be one ofvalid,invalid,unknown,catch-all, orout-of-storage.
# Verify a single email
curl -X POST https://app.optimaverifier.com/api/v1/verifications/single \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{"email": "ada.lovelace@example.com"}'Authentication
Authenticate requests with an Optima API key in the standard bearer-token header.
API keys
API keys identify the account that owns the request and determine the credit balance, rate limits, and job history used by the call.
Server-side only
Send API requests from your backend, worker, or command-line tooling. Do not embed API keys in frontend JavaScript, mobile apps, or public repositories.
Key format
All keys use the sk_live_ prefix. Treat them as secrets and never commit them to source control.
Authorization: Bearer <api_key> Content-Type: application/json
{
"detail": "Could not validate credentials"
}Base URL
A single versioned host serves every customer. Your API key identifies the account.
The production API base is:
All endpoint paths in these docs are relative to this base URL.
Versioning
The path is versioned (/api/v1/…). Breaking changes get a new major version. v1 will be supported for at least 12 months after v2 ships.
https://app.optimaverifier.com/api/v1/{resource}
// e.g.
https://app.optimaverifier.com/api/v1/verifications/single
https://app.optimaverifier.com/api/v1/verifications/bulk
https://app.optimaverifier.com/api/v1/verifications/job/{job_id}Verify one email
Synchronous verification for signup forms and account changes. Response time depends on the receiving provider; set a request timeout and handle inconclusive results.
Body parameters
| emailrequired string | The email address to verify. Lowercased server-side. Plus-addressing is preserved (e.g. ada+lists@example.com is checked as-is). |
Returns
An EmailVerifyResponse with the resolved status, an ISO-8601 verified_attimestamp, and the user's updated credits_remaining. The same email submitted within the cache window is served from cache and does not consume a credit.
Status codes
| 200 | Verification complete (regardless of result - invalid is still 200). |
| 401 | Missing or invalid API key. |
| 422 | Email syntactically invalid or empty. |
| 429 | Rate limit or monthly usage limit exceeded. See Retry-After header. |
curl -X POST https://app.optimaverifier.com/api/v1/verifications/single \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{"email": "ada.lovelace@example.com"}'{
"email": "ada.lovelace@example.com",
"status": "valid",
"verified_at": "2026-05-25T10:30:00.123456+00:00",
"credits_remaining": 4999
}Bulk verify · submit a JSON array
Submit up to 1 million emails per request as a JSON array. We deduplicate, queue, and return a job id immediately.
Workflow
queued. Deduplication happens before queueing - you're only billed for unique emails.Body parameters
| emailsrequired array<string> | List of addresses to verify. Min 1, max 1,000,000 per request. Duplicates are removed before billing - you only pay for unique addresses. |
| job_namerequired string · 1–255 chars | Label shown in the dashboard and search. |
| row_metadataoptional object | Per-email metadata keyed by email address. Echoed back in CSV export so you can join results to your source rows. |
| extra_columnsoptional array<string> | Ordered list of metadata keys to include as columns in the CSV export. |
| track_finalityoptional boolean · default false | Track advanced checks for new jobs. Existing response is unchanged. Use finality to wait for stable results. Unavailable enrollment returns 503 before a new charge. |
| forceoptional boolean · default false | Bypass duplicate reuse and start a normally charged new run. Reusing an untracked job does not convert it to tracked. |
Returns
A BulkJobResponse with status queued, the deduplicated total_rows, and duplicate_count showing how many duplicates were removed before queueing.
curl -X POST https://app.optimaverifier.com/api/v1/verifications/bulk \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{
"emails": ["ada@example.com", "marcus@brand.co", "rachel@nope.invalid"],
"job_name": "May newsletter sweep"
}'{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"job_name": "May newsletter sweep",
"status": "queued",
"total_rows": 2964,
"original_count": 3001,
"duplicate_count": 37,
"processed_rows": 0,
"valid_count": 0,
"invalid_count": 0,
"unknown_count": 0,
"catch_all_count": 0,
"created_at": "2026-05-25T10:30:00+00:00",
"completed_at": null,
"credits_reserved": 2964
}Bulk verify · upload a file
For lists too large to JSON-encode. Send a multipart upload with a .txt file - one email per line.
Form fields
| filerequired file · .txt | One email address per line. Only .txt files are accepted. For CSV input, extract the email column client-side and use the JSON endpoint instead. |
| job_namerequired string | Label shown in the dashboard. |
Returns
Same BulkJobResponse shape as the JSON endpoint. Optional form fields track_finality=true and force=true have the same semantics and default to false. Track with the polling endpoint, or wait for finality.
curl -X POST https://app.optimaverifier.com/api/v1/verifications/bulk/upload \ -H "Authorization: Bearer <api_key>" \ -F "file=@my-list.txt" \ -F "job_name=Q2 contacts"
Poll a job
Poll every 5–15 seconds. Save the job ID and bound both each request and your overall wait.
Existing/fast workflow (unchanged): completed means the main pass is ready to download. Advanced checks may still update unknown/catch-all addresses. Integrations needing a stable result set can opt into finality tracking; existing clients need no migration.
Returns
A BulkJobStatus with progress_percentage and the per-status counts as they accumulate.
The runnable JavaScript and Python examples handle every main job state, read retries and a two-hour client deadline. A client timeout does not cancel the job. Keep the ID to resume later.
These are read-only examples: set OPTIMA_API_KEY and OPTIMA_JOB_ID. JavaScript requires Node.js 20+ (an .mjs file); Python requires requests. The cURL tab makes one status request.
# One status read, not a polling loop. Save JOB_ID from your submission. curl --fail-with-body --connect-timeout 10 --max-time 30 -sS \ "https://app.optimaverifier.com/api/v1/verifications/job/$JOB_ID" \ -H "Authorization: Bearer $OPTIMA_API_KEY" # Inspect status using the lifecycle table. completed is main-pass completion only. # For a bounded wait-and-export Bash example, see Advanced verification & finality.
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "processing",
"progress_percentage": 47.3,
"total_rows": 2964,
"processed_rows": 1402,
"valid_count": 1284,
"invalid_count": 95,
"unknown_count": 17,
"catch_all_count": 6
}Advanced verification & finality
Know when results stop changing, not just when the main pass reaches 100%.
1. Opt in when creating a bulk job
Send track_finality: true in the JSON request, or track_finality=true in a multipart TXT upload. The default is false. A newly created job returns 201; duplicate reuse returns 200 and reused: true, with no additional charge. Both use the same legacy response shape. Save the returned ID.
Enrollment is rolled out gradually. An explicit 503 enrollment rejection happens before a new job or charge; honor Retry-After. Do not silently fall back to untracked results if your integration requires a stable report.
Opting in does not enroll reused historical jobs. If a reused job is not_tracked, stop waiting. Accept the legacy result or explicitly submit with force: true to create a new, normally charged run. Never force automatically after a timeout.
2. Read the companion finality endpoint
Use your existing bearer key. Reads do not start checks or consume credits and return Cache-Control: no-store. The batch query accepts comma-separated public IDs, deduplicates them in request order, and returns {"items": [...]}. Empty IDs, IDs longer than 36 characters, or more than 100 distinct IDs return 422. Any missing or foreign ID makes the entire batch return the same generic 404.
| Field | Meaning |
|---|---|
results_final | true: sealed after successful main processing. false: tracked but not final, including aborted. null: untracked. Only the boolean true authorizes a final-results workflow. |
results_revision | Nullable integer observation token for committed result inserts/classification changes. A batch may increment once. Not a row count or retrievable historical export version. |
results_updated_at / finalized_at | Nullable UTC timestamps: last committed result change / time the result set was sealed. These do not replace completed_at or per-row verified_at. |
total / pending / finished | Logical checks, not email addresses or retry attempts. One address may have several checks. total = pending + finished. Pending includes queued, running, delayed retries and uncommitted writebacks. |
upgraded / failed / expired | upgraded counts distinct addresses with a committed follow-up classification change. failed and expired count checks and are subsets of finished, not extra checks to add to it. |
deadline_at | Nullable UTC follow-up deadline, currently configured initially as 60 minutes after main completion. Use the returned timestamp, not a hardcoded SLA. Finalization may be observed on a subsequent poll. |
Nulls | For not_tracked, every counter, revision, timestamp and stop_reason is null, not zero. Do not coerce missing tracking into a successfully finished job. |
Response examples
Every example is a complete single-job response. For batch reads, place these objects inside items. See the state tables for the action associated with each state.
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "completed",
"results_final": false,
"results_revision": 12,
"results_updated_at": "2026-09-14T09:12:00Z",
"finalized_at": null,
"additional_checks": {
"status": "running",
"total": 5,
"pending": 2,
"finished": 3,
"upgraded": 2,
"failed": 1,
"expired": 0,
"deadline_at": "2026-09-14T10:00:00Z",
"stop_reason": null
}
}3. Wait, inspect the outcome, then export
Wait for the boolean results_final === true before beginning pagination or downloading a stable CSV. A main completed status, zero pending checks, or 100% progress alone is insufficient.
Final means sealed, not that every address became valid or invalid. Check failed, expired and stop_reason; unknown and catch-all addresses may remain. At the follow-up deadline, outstanding checks expire and a complete main result set is sealed as-is.
Export through the existing route. Before sealing, pages, filtered totals and exports can change; previously downloaded files do not update themselves. CSV columns remain email,status,verified_at, followed by supplied extra columns.
These end-to-end examples submit a billable job once, poll with request and overall timeouts, and export only after finality. JavaScript: Node.js 20+, save as .mjs. Python: Python 3 plus requests. Shell: Bash, cURL, jq and GNU date. Set OPTIMA_API_KEY server-side first.
In production, persist the returned job ID before waiting. To resume, call the wait function with your saved ID instead of rerunning the submission. For large lists, stream the CSV to storage instead of printing or loading it all into memory.
Examples stop on paused, aborted, untracked, malformed or unfamiliar states. A two-hour client timeout is an example policy, not a completion SLA. Keep the ID and investigate or resume later.
# Bash, curl and jq required. OPTIMA_API_KEY is a server-side environment variable.
set -euo pipefail
BASE=https://app.optimaverifier.com/api/v1/verifications
JOB=$(curl --fail-with-body --connect-timeout 10 --max-time 30 -sS "$BASE/bulk" \
-H "Authorization: Bearer $OPTIMA_API_KEY" -H 'Content-Type: application/json' \
-d '{"emails":["ada@example.com"],"job_name":"Tracked run","track_finality":true}')
# Submit 503 means enrollment unavailable, with no new charge. Do not blindly retry POST.
JOB_ID=$(jq -er '.id' <<< "$JOB")
echo "Save job ID: $JOB_ID"
END=$((SECONDS + 7200))
FINAL=false
POLL_DIR=$(mktemp -d)
trap 'rm -f "$POLL_DIR/headers" "$POLL_DIR/body"; rmdir "$POLL_DIR"' EXIT
while (( SECONDS < END )); do
REMAINING=$((END - SECONDS))
REQUEST_TIMEOUT=$((REMAINING < 30 ? REMAINING : 30))
if ! CODE=$(curl --connect-timeout 10 --max-time "$REQUEST_TIMEOUT" -sS \
-D "$POLL_DIR/headers" -o "$POLL_DIR/body" -w '%{http_code}' \
-H "Authorization: Bearer $OPTIMA_API_KEY" "$BASE/job/$JOB_ID/finality"); then
REMAINING=$((END - SECONDS))
(( REMAINING <= 0 )) && break
sleep $((REMAINING < 10 ? REMAINING : 10)); continue
fi
if [[ "$CODE" == 429 || "$CODE" == 5?? ]]; then
RETRY=$(awk 'tolower($1)=="retry-after:" {$1=""; sub(/^ /, ""); sub(/\r$/, ""); print}' "$POLL_DIR/headers")
DELAY=10
if [[ "$RETRY" =~ ^[0-9]+$ ]]; then DELAY=$RETRY
elif [[ -n "$RETRY" ]]; then
# GNU date handles Retry-After's HTTP-date form.
WHEN=$(date -d "$RETRY" +%s 2>/dev/null || date +%s)
DELAY=$((WHEN - $(date +%s)))
fi
(( DELAY < 1 )) && DELAY=1
REMAINING=$((END - SECONDS))
(( REMAINING <= 0 )) && break
sleep $((DELAY < REMAINING ? DELAY : REMAINING)); continue
fi
[[ "$CODE" == 200 ]] || { echo "Finality HTTP $CODE; stop and fix credentials/request"; exit 1; }
jq -e . "$POLL_DIR/body" >/dev/null
if ! jq -e '(.status == "queued" or .status == "processing" or .status == "completed" or .status == "error" or .status == "canceled") and (.additional_checks.status == "waiting" or .additional_checks.status == "running" or .additional_checks.status == "finished" or .additional_checks.status == "aborted" or .additional_checks.status == "not_tracked")' "$POLL_DIR/body" >/dev/null; then
echo 'Paused or unrecognized state; retain the ID and investigate'; exit 1
fi
if jq -e '.status == "error" or .status == "canceled" or .additional_checks.status == "aborted" or .additional_checks.status == "not_tracked" or .results_final == null' "$POLL_DIR/body" >/dev/null; then
echo 'Run aborted/untracked; stop waiting. Forcing a new run costs credits.'; exit 1
fi
if jq -e '.results_final == true and .status == "completed" and .additional_checks.status == "finished"' "$POLL_DIR/body" >/dev/null; then
FINAL=true; jq '.additional_checks' "$POLL_DIR/body"; break
fi
jq -e '.results_final == false and .additional_checks.status != "finished"' "$POLL_DIR/body" >/dev/null || { echo 'Inconsistent finality response'; exit 1; }
REMAINING=$((END - SECONDS))
(( REMAINING <= 0 )) && break
sleep $((REMAINING < 10 ? REMAINING : 10))
done
[[ "$FINAL" == true ]] || { echo 'Timed out; retain the job ID'; exit 1; }
curl --fail-with-body --max-time 60 -sS -H "Authorization: Bearer $OPTIMA_API_KEY" \
"$BASE/job/$JOB_ID/export?format=csv"Read paginated results
Per-row results for a completed (or in-progress) bulk job.
For a stable report, wait for finality before requesting page 1. Otherwise rows, classifications and filtered totals may change between requests. total_count describes the selected filter; total_pages is at least 1 even for an empty result set. Stop on an empty page or when you have consumed the returned total.
Query parameters
| pageoptional integer · default 1 | 1-indexed; values below 1 are clamped to 1. |
| per_pageoptional integer · default 50 | Page size, clamped to 1–100. |
| status_filteroptional string | Filter to valid, invalid, unknown, catch-all, or out-of-storage. The invalid and unknown filters return the same rows as before; out-of-storage lists full mailboxes, which are also inside invalid or unknown. |
curl "https://app.optimaverifier.com/api/v1/verifications/job/<job_id>/results?page=1&per_page=50" \ -H "Authorization: Bearer <api_key>"
{
"id": "a1b2c3d4-...",
"job_name": "May newsletter sweep",
"total_count": 2964,
"page": 1,
"per_page": 50,
"total_pages": 60,
"results": [
{ "email": "ada@example.com", "status": "valid", "verified_at": "2026-09-14T09:12:00+00:00" },
{ "email": "rachel@nope.invalid", "status": "invalid", "verified_at": "2026-09-14T09:12:00+00:00" }
]
}Export CSV
Stream the full per-row result set as a CSV file. Echoes back any row_metadata columns you submitted.
The main job must be completed; otherwise export returns 400. For stable results, additionally wait for results_final === true. Use a temporary file for large downloads and publish it only after the download succeeds.
Query parameters
| formatoptional string · default csv | Only csv is supported. |
CSV shape
One row per unique submitted email. Default columns: email, status, verified_at. If extra_columns was supplied at submit time, those keys are appended in the order given. Before finality, exports and pagination/filter totals may change. For a stable complete set, wait for finality before reading/exporting. Previously downloaded files do not update themselves.
curl --fail --connect-timeout 10 --max-time 300 "https://app.optimaverifier.com/api/v1/verifications/job/<job_id>/export?format=csv" \ -H "Authorization: Bearer <api_key>" \ -o results.csv
Custom report
Download only the rows you need: pick the statuses, and optionally keep or drop free-mailbox and role addresses. Check the row count first, then download.
The main job must be completed to download. This is separate from Export CSV, which is unchanged.
Query parameters
| statusesrequired string · comma-separated | Any of valid, invalid, unknown, catch-all, out-of-storage. Unknown values return 400. When empty, the count endpoint returns 0 and the download returns 400. |
| freeoptional string · yes | no | yes keeps only consumer-mailbox addresses (Gmail, Yahoo, Outlook and similar); no keeps only the rest. Omit it, or send an empty value, for no filter. |
| roleoptional string · yes | no | yes keeps only team or function addresses (info@, support@, sales@ and similar); no keeps only the rest. Omit it for no filter. When both free and role are set, a row must match both. |
Count response
{"total_count": 105}, sent with Cache-Control: no-store. It is exactly the number of data rows the download returns for the same parameters.
CSV shape
Columns: email, status, free (yes/no), role (yes/no), verified_at, then any extra_columns you supplied at job creation.
Statuses do not overlap here
In this report an out-of-storage address appears only under out-of-storage, never also under invalid or unknown. That differs from the job totals and the results status_filter, which still include those addresses in invalid_count or unknown_count. Selecting every status returns every row once.
Free and role are worked out from the address, so they also work on jobs created before this endpoint existed. A missing or unknown job returns 404.
# 1. How many rows would the report contain?
curl --fail --connect-timeout 10 --max-time 60 \
"https://app.optimaverifier.com/api/v1/verifications/job/<job_id>/custom-report/count?statuses=out-of-storage,unknown&free=yes" \
-H "Authorization: Bearer <api_key>"
# {"total_count": 105}
# 2. Download exactly those rows
curl --fail --connect-timeout 10 --max-time 300 \
"https://app.optimaverifier.com/api/v1/verifications/job/<job_id>/custom-report?statuses=out-of-storage,unknown&free=yes" \
-H "Authorization: Bearer <api_key>" \
-o custom_report.csvVerification history
All single verifications the user has run, newest first. Bulk results live on each job's /results endpoint instead.
Query parameters
| pageoptional integer · default 1 | 1-indexed. |
| per_pageoptional integer · default 50 | Page size. |
curl "https://app.optimaverifier.com/api/v1/verifications/history?page=1&per_page=50" \ -H "Authorization: Bearer <api_key>"
Find existing jobs
Recover a job ID or reconcile a submission whose response was interrupted.
Query parameters: page (default 1), per_page (default 50, clamped to 1–100), optional status (main job state), and optional search (job-name match).
The response is a JSON array of BulkJobResponse objects, not an items wrapper. An empty array means no matching jobs on that page. Use the same bearer authentication.
GET https://app.optimaverifier.com/api/v1/verifications/jobs?page=1&per_page=50&search=My%20import Authorization: Bearer <api_key>
After an ambiguous POST timeout, check here or in the dashboard before resubmitting. Names and list matching are not an idempotency guarantee. Do not invent an Idempotency-Key contract or automatically use force=true.
Check balance
Returns the current credit balance and the 20 most recent ledger entries.
Returns
| balance integer | Current credit balance. |
| recent array | Up to 20 most recent ledger entries. Each entry has delta (positive = credit, negative = debit), reason, balance_after, optional note, and created_at. |
curl "https://app.optimaverifier.com/api/v1/credits" \ -H "Authorization: Bearer <api_key>"
{
"balance": 4999,
"recent": [
{
"delta": -1,
"reason": "consume",
"balance_after": 4999,
"note": null,
"created_at": "2026-05-25T10:30:00+00:00"
}
]
}Status vocabulary
These are per-email classifications, not job states or HTTP error codes.
| Email status | Meaning |
|---|---|
valid | Verification found evidence that the address can receive mail. This is not a guarantee of delivery, inbox placement, or recipient consent. |
invalid | Verification found the address undeliverable, for example an invalid address or rejected mailbox. Exclude it from sending. |
unknown | Available checks could not establish a definitive result. Keep it separate from valid addresses; eligible follow-up checks may change it. |
catch-all | The domain accepts arbitrary recipients, so acceptance alone cannot confirm this mailbox. Treat it as uncertain; eligible follow-up checks may change it. |
out-of-storage | The mailbox exists but is full or over its quota, so mail is not being accepted right now. It is not a verdict that the address is invalid; do not send now, and it may accept mail later. In job totals these addresses are still counted inside invalid_count or unknown_count. List them with status_filter=out-of-storage. |
HTTP 200 means the API request succeeded, even when an email is invalid. Until a tracked job is final, an address’s classification can change through advanced verification.
Errors
API errors include a detail field; it may be a string, an object or a list. Intermediaries may return non-JSON errors, so check HTTP status before parsing.
| HTTP | Meaning | Recovery |
|---|---|---|
400 | Unsupported input, empty upload, unsupported export format, or export requested before main completion. | Fix the request or wait for completed before exporting. |
401 | Missing, invalid or expired credentials. | Fix or rotate credentials; do not retry unchanged. |
402 | Insufficient credits or overdue subscription payment. | Resolve billing. Inspect detail; it can be an object, not just a string. |
403 | Account/key is not allowed to use this operation. | Check API access and account permissions. |
404 | Job is missing or not owned by the caller. | Check the ID and account. A batch finality read fails as a whole if any ID is unavailable. |
422 | Request validation failed. | Inspect detail (often a list of field errors), fix the payload or query. |
429 | Rate or usage limit reached. | Honor Retry-After when present. A usage cap can require an account change; repeated immediate retries will not help. |
500 / other 5xx | Server or intermediary failure. | Retry read-only GET requests with a bounded delay. A failed/timeout POST may already have been accepted; reconcile before resubmitting. |
503 | Verification maintenance or new-job finality enrollment unavailable. | Honor Retry-After. An explicit enrollment rejection creates no new job or charge. Do not silently disable tracking if stable results are required. |
Retry read-only GET requests on transient failures with a bounded delay (the polling examples use 10 seconds by default). Honor Retry-After as seconds or an HTTP date; cap waits at your overall deadline. Add jitter when polling many jobs.
Do not blindly retry POST after a timeout or 5xx: the job may already exist and credits may have been reserved. Reconcile first. An SMTP refusal or timeout is an email-verification outcome, not automatically an HTTP 503.
{
"detail": [
{
"type": "value_error",
"loc": ["body", "emails"],
"msg": "List should have at most 1000000 items",
"input": []
}
]
}Quotas & limits
Per-user limits enforced server-side.
| Single verify rate 15 / minute / user | Sliding window. Returns 429 with Retry-After when exceeded. |
| Bulk JSON size 1,000,000 emails | Per request. Split larger lists into multiple jobs. |
| Bulk upload size ~200 MB | Approximate; depends on average line length. Use the JSON endpoint for tighter framing. |
| Concurrent bulk jobs No hard cap | Jobs share worker capacity; completion time depends on list size, providers and queue load. Do not assume strict FIFO completion or a fixed latency. |
| Result retention 30 days | Per-row results and CSV exports for a bulk job. Aggregate counts are kept indefinitely. |
Before going live
Test your integration against outcomes, not just the happy path.
- Keep keys server-side. Persist job IDs durably and redact credentials from logs.
- Choose preliminary or stable results explicitly. Keep email, main-job and advanced-check states separate.
- Test queued → processing → completed with checks still running; never export early in a stable-results workflow.
- Test natural finality, no checks, failed checks and deadline expiry. Do not turn unknown or catch-all into valid in your app.
- Test duplicate reuse of an untracked job, paused jobs, error, canceled, incomplete and an unfamiliar future state.
- Test 401/402/403/404/422, 429 with Retry-After, 503 enrollment unavailable and a network timeout after POST.
- Use bounded GET retries and polling deadlines. Client timeouts do not cancel jobs; retain the ID to resume.
- For stable reports, wait before the first page/export. Use returned pagination totals; avoid combining pages fetched before and after result changes.
- Stream large downloads. Preserve the unchanged CSV header and supplied metadata columns.
When escalating a problem, include the public job ID, endpoint, HTTP status, UTC time, main status, finality status, revision and stop reason. Never send your bearer key.