OptimaVerifierv1
Back to app
v1 · integration guide

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.

Use your Optima API key for every request. Send it in theAuthorization 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

statusMeaningYour client should…
queuedAccepted; waiting for worker capacity.Keep the job ID and poll. Do not resubmit because progress is zero.
processingThe main verification pass is running.Poll every 5–15 seconds. Partial rows and counts can change.
pausedWork 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.
completedThe main pass finished. CSV export is available.Use results now if preliminary results are acceptable. For stable results, also require results_final === true.
errorThe main job failed.Stop waiting; investigate. Partial results are not a complete report. Do not automatically submit a replacement.
canceledThe 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.statusMeaningYour client should…
waitingTracked; the main pass can still discover checks, but none have been registered yet.Keep polling. Zero pending is not proof of finality.
runningTracked follow-up work is active, or its producer is closing.Keep polling even if the main job is completed or pending is zero.
finishedThe tracked result set has been sealed.Require results_final === true, inspect failed/expired/stop_reason, then download.
abortedTracking ended without a successfully completed, sealed result set.Stop waiting. Inspect stop_reason; do not treat this as a final successful report.
not_trackedThis 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_reasonMeaning
nullNo terminal reason yet, or the job is untracked. Check additional_checks.status.
completedAll registered checks reached a terminal outcome. Some can have failed or remained inconclusive.
no_checksThe main pass completed and no follow-up checks were needed.
deadlineThe follow-up time limit was reached; remaining checks expired. A complete main result set is sealed as-is.
errorThe main job failed; finality was aborted.
canceledThe main job was canceled; finality was aborted.
incompleteThe 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.

  1. Create or copy an API key from the dashboard.
  2. Send a POST request to https://app.optimaverifier.com/api/v1/verifications/single with your key in the Authorization header.
  3. Read status from the response. It will be one of valid, invalid, unknown, catch-all, or out-of-storage.
Once it works, try bulk verification for lists, or read about error shapes before putting code into production.
# 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.

Never expose an API key to a browser. Anything that authenticates your account belongs in a server-side environment, behind your own auth layer.

Key format

All keys use the sk_live_ prefix. Treat them as secrets and never commit them to source control.

Authorization header
Authorization: Bearer <api_key>
Content-Type:  application/json
If the key is missing or invalid401 Unauthorized
{
  "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:

https://app.optimaverifier.com/api/v1

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.

URL anatomy
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.

POSThttps://app.optimaverifier.com/api/v1/verifications/single~800 ms p5015 req / min1 credit

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

200Verification complete (regardless of result - invalid is still 200).
401Missing or invalid API key.
422Email syntactically invalid or empty.
429Rate 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"}'
Valid · delivery confirmed200 OK
{
  "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.

POSThttps://app.optimaverifier.com/api/v1/verifications/bulkasync1M max1 credit / unique email

Workflow

01 · Submit
POST your list
POST /verifications/bulk
Get back a job id with status queued. Deduplication happens before queueing - you're only billed for unique emails.
02 · Poll
Watch progress
GET /verifications/job/{id}
Poll every 5–15s. Handle all lifecycle states, including paused, error and canceled.
03 · Download
Export results
GET /verifications/job/{id}/export?format=csv
Stream the per-row CSV. Results stay available for 30 days.

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"
  }'
Response · job queued201 Created
{
  "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.

POSThttps://app.optimaverifier.com/api/v1/verifications/bulk/uploadmultipart/form-data1M rowsTXT only

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.

GEThttps://app.optimaverifier.com/api/v1/verifications/job/{job_id}lightweightfree to poll

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.
Response · in progress200 OK
{
  "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

GEThttps://app.optimaverifier.com/api/v1/verifications/job/{job_id}/finalityopt-infree to pollbackward compatible
GEThttps://app.optimaverifier.com/api/v1/verifications/jobs/finality?ids=…1–100 IDssame ownership rules

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.

FieldMeaning
results_finaltrue: sealed after successful main processing. false: tracked but not final, including aborted. null: untracked. Only the boolean true authorizes a final-results workflow.
results_revisionNullable 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_atNullable 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 / finishedLogical 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 / expiredupgraded 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_atNullable 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.
NullsFor 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.

200 OK · Main completed; advanced checks running
{
  "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.

GEThttps://app.optimaverifier.com/api/v1/verifications/job/{job_id}/resultspaginatedoptional status filter

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>"
Response · page of results200 OK
{
  "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.

GEThttps://app.optimaverifier.com/api/v1/verifications/job/{job_id}/exportstreamingCSV

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.

GEThttps://app.optimaverifier.com/api/v1/verifications/job/{job_id}/custom-report/countJSON
GEThttps://app.optimaverifier.com/api/v1/verifications/job/{job_id}/custom-reportstreamingCSV

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.csv

Verification history

All single verifications the user has run, newest first. Bulk results live on each job's /results endpoint instead.

GEThttps://app.optimaverifier.com/api/v1/verifications/historypaginated

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.

GEThttps://app.optimaverifier.com/api/v1/verifications/jobsowned jobsnewest first

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.

GEThttps://app.optimaverifier.com/api/v1/creditsfree to call

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>"
Response200 OK
{
  "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 statusMeaning
validVerification found evidence that the address can receive mail. This is not a guarantee of delivery, inbox placement, or recipient consent.
invalidVerification found the address undeliverable, for example an invalid address or rejected mailbox. Exclude it from sending.
unknownAvailable checks could not establish a definitive result. Keep it separate from valid addresses; eligible follow-up checks may change it.
catch-allThe domain accepts arbitrary recipients, so acceptance alone cannot confirm this mailbox. Treat it as uncertain; eligible follow-up checks may change it.
out-of-storageThe 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.

HTTPMeaningRecovery
400Unsupported input, empty upload, unsupported export format, or export requested before main completion.Fix the request or wait for completed before exporting.
401Missing, invalid or expired credentials.Fix or rotate credentials; do not retry unchanged.
402Insufficient credits or overdue subscription payment.Resolve billing. Inspect detail; it can be an object, not just a string.
403Account/key is not allowed to use this operation.Check API access and account permissions.
404Job 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.
422Request validation failed.Inspect detail (often a list of field errors), fix the payload or query.
429Rate 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 5xxServer or intermediary failure.Retry read-only GET requests with a bounded delay. A failed/timeout POST may already have been accepted; reconcile before resubmitting.
503Verification 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.

Example error body422 Unprocessable
{
  "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.