Upload a CSV file and start a batch validation job
Download OpenAPI specUpload a CSV file of email addresses and start a batch validation job. The CSV
must have an email column; other columns are passed through to the result file.
Credits are deducted immediately on job start based on the number of valid email
rows. When insufficient credits: 402 with shortfall, balance, and required
in the body.
Test mode — a ev_test_ key runs the batch for free (charged: 0) with no
DNS or SMTP. Each row's outcome is derived from the local part of its address
(the same convention as single verify — valid@, invalid@, catch_all@,
disposable@, role@, unknown@), and the values are canned (produced
without running the live validation pipeline), so a test batch matches single
verify exactly:
| Local part | Outcome |
Risk |
Flags |
|---|---|---|---|
valid (or anything not listed) |
deliverable |
none |
— |
role |
deliverable |
medium |
role |
catch_all |
catch-all |
medium |
catch-all |
invalid |
invalid |
high |
— |
disposable |
invalid |
high |
disposable |
unknown |
unknown |
low |
— |
Test batches are capped at 1,000 valid rows; over the cap the request returns
400 with code: too_many_rows (the machine-readable field — error carries prose;
branch on code). The result is a real downloadable CSV/JSON
(pre-signed URLs) and, when callback_url is supplied, a real signed
batch.completed webhook — the same shapes a live batch produces. Test batches do
not affect your account's lifetime dashboard stats.
Idempotency — send an Idempotency-Key and a retry replays the original response
instead of buying a second job. The request identity is the file NAME, a SHA-256 of the
file's CONTENTS, the email column and the callback_url. Because the digest is taken over
the decoded file bytes alone, re-sending the same file replays even if the multipart
boundary, the chunk sizes or the transfer encoding differ — while a genuinely different
file under the same key is refused as reuse (422 idempotency_key_reuse), never answered
with the other file's results.
Requires the verify scope.
Parameters
header · parameters
| Parameter | Type | Description |
|---|---|---|
| Idempotency-Key | string |
Client-generated key (up to 255 characters, one per upload) that makes a retry safe. A repeat with the same upload replays the stored response verbatim and adds Idempotency-Replayed: true; a repeat with a different upload is a 422; a repeat while the first is still in flight is a 409. Records expire after 24 hours and are scoped to the account, the endpoint and the key's mode. If the first request charged for the batch and then died before answering, the retry gets 202 carrying the BatchJobResponse for the job that charge created. A replayed retry still transfers its bytes — the file name is only known while streaming the body — but is never charged twice.
example: 00000000-0000-4000-8000-000000000001
|
Request body · multipart/form-data · required
| Field | Type | Description |
|---|---|---|
| file required | string · binary |
The address list to validate — a .csv, .txt or .xlsx file. Any other extension is rejected with code: unsupported_file_type. Columns other than the email column are passed through to the result file unchanged.
|
| callback_url | string · uri | Optional HTTPS URL to receive a webhook when the batch job completes. |
| email_column | string |
Optional name of the column holding the addresses. Needed only when several columns could be the email column, which is answered with 422 needs_column_selection and the candidate names — re-send with this field set to one of them. The API never guesses, because guessing would risk billing for the wrong column.
example: work_email
|
| has_header | string |
Optional true/false override for header detection, for the rare file whose first row is misread as (or mistaken for) a header.
example: true
|
Responses
Idempotency recovery. A previous request with this Idempotency-Key charged for the
batch and then died before it could answer (a crash, a deploy, a dropped connection).
The job exists; this response is that job, carries Idempotency-Replayed: true, and
costs nothing. The batch is never started twice.
The body is the same BatchJobResponse that GET /api/v1/verify/batch/{id} returns —
not a summary of it — so a recovery reports the job's CURRENT status and, if it has
already finished, its csv_url and json_url straight away. There is no earlier
response to replay here, so this is recomputed on every retry rather than frozen: it can
never tell you a job is running hours after it completed.
code: invalid_request), missing file field (code: invalid_request), a file extension outside the allow-list (code: unsupported_file_type), or a rejected callback_url (code: invalid_callback_url). For ev_test_ keys, a batch exceeding the 1,000-row test cap returns code: too_many_rows.
· application/json
code: insufficient_scope). It is a 403 rather than a 401 on purpose — the credential is valid, so retrying with it can never help; a key holding the verify scope is needed.
· application/json
shortfall, balance, and required.
· application/json
The resource a charged Idempotency-Key paid for can no longer be read (code: idempotency_resource_gone). The charge committed and the thing it created has since
become unreadable — an account erasure, or the 7-day retention delete landing inside the
key's 24-hour life.
It carries its OWN code rather than the generic not_found, because the two mean opposite
things: not_found is an ordinary miss you may have caused, while this is a server-side
fault worth reporting. Retrying the same key cannot succeed.
email_column set to the correct column name.
· application/json
Response schema · 200 · BatchStartResponse
POST /api/v1/verify/batch when a job is accepted.| Field | Type | Description |
|---|---|---|
| job_id | string · uuid | Unique identifier for the batch job. example: 7a4e2c1b-0000-0000-0000-000000000000 |
| livemode | boolean |
true when the job was submitted with a LIVE key, false for an ev_test_ sandbox key whose rows get canned verdicts and cost no credits. Read from the created job's stored mode, so it cannot disagree with what a later GET of the same job reports. It rides on this body — the first one you see for a new job, and the one you are likely to log — rather than only on the reads.
example: true
|
| total_rows | integer | Total number of rows in the uploaded file. example: 1000 |
| skipped_rows | integer | Number of non-email/blank rows skipped during the upload scan. example: 5 |
| charged | integer | Number of email addresses billed (valid-email rows only). example: 995 |
| credits_remaining | integer · null |
Credit balance after deduction. null for system/demo accounts.
example: 4817
|
| status | string |
Always processing immediately after job creation.
processing
example: processing
|
| callback_secret | string |
Per-job HMAC secret. Included only when callback_url was provided in the request; it is not retrievable later. Supplying/keeping it is optional — it OVERRIDES the account signing secret for this job's delivery only. The documented default is your account signing secret (whsec_…), which you can pin once instead of tracking one secret per job. See components.headers.X-Signature.
example: bv_1c3e5a70...
|
Response schema · 202 · BatchJobResponse
GET /api/v1/verify/batch/{id}.| Field | Type | Description |
|---|---|---|
| job_id | string · uuid | example: 7a4e2c1b-0000-0000-0000-000000000000 |
| livemode | boolean |
true when the job was submitted with a LIVE key; false when it came from an ev_test_ sandbox key. A key only ever lists — and can only ever fetch — jobs of its own mode.
example: true
|
| status | string |
The job's lifecycle stage. POST /api/v1/verify/batch answers processing at creation — a submission acknowledgement rather than a stage — so treat anything that is not complete or failed as still working, and a stage added later will not break you.
pendingapproval
resolving
smtping
assembling
complete
failed
example: complete
|
| total_rows | integer | Total rows in the uploaded file. example: 1000 |
| billed_rows | integer | Number of rows that were billed. example: 995 |
| original_file_name | string | File name as uploaded. example: emails.csv |
| created_at | string · date-time | example: 2025-06-10T14:00:00Z |
| started_at | string · date-time · null | example: 2025-06-10T14:00:01Z |
| completed_at | string · date-time · null |
UTC timestamp when validation completed. null while processing.
example: 2025-06-10T14:00:45Z
|
| expires_at | string · date-time · null |
UTC timestamp after which result URLs are no longer valid. null while processing.
example: 2025-06-17T14:00:45Z
|
| csv_url | string · null |
Pre-signed download URL for the CSV result file. Valid for 7 days. null while processing.
example: https://storage.example.com/results/job.csv?X-Amz-Expires=604800
|
| json_url | string · null |
Pre-signed download URL for the JSON result file. Valid for 7 days. null while processing.
example: https://storage.example.com/results/job.json?X-Amz-Expires=604800
|
Error responses
400 401 403 404 409 413 429 500 · ApiProblem · object
| Field | Type | Description |
|---|---|---|
| status required | integer | HTTP status code mirrored in the body. example: 400 |
| code required | string |
Stable, machine-readable error code. Branch your code on this — never on the error text, which is prose and may be reworded at any time.
example: invalid_email
|
| error required | string | Human-readable error message. example: Invalid email format. |
| param | string | The request field the failure is attributable to. Omitted when the failure is not about one specific field. example: email |
| details | array of string · null | Optional per-field validation messages. Null when not applicable. example: ["email: required"] |
| doc_url | string | Link to the documentation for this error code. example: https://emailvalidator.ai/docs/get-started/errors |
| request_id | string | Correlation id for this request. Quote it in a support request — it is how a single call is found in the server logs. example: 0HNCV1TQ3PLRK:00000003 |
402 · InsufficientCreditsProblem · object
402 body of a batch submission: the standard error envelope plus the typed credit numbers, kept top-level so a client can top up by shortfall and retry without parsing prose.| Field | Type | Description |
|---|---|---|
| status required | integer | HTTP status code mirrored in the body. example: 402 |
| code required | string |
Stable, machine-readable error code.
insufficient_credits
example: insufficient_credits
|
| error required | string | Human-readable error message. example: Insufficient credits. |
| shortfall required | integer | Credits missing for this batch — buy at least this many, then retry. example: 5 |
| balance required | integer | The account's credit balance when the batch was submitted. example: 5 |
| required required | integer | Credits the batch would have cost. example: 10 |
| doc_url | string | Link to the documentation for this error code. example: https://emailvalidator.ai/docs/get-started/errors |
| request_id | string | Correlation id for this request. example: 0HNCV1TQ3PLRK:00000003 |
422 · NeedsColumnSelectionProblem · object
422 body of a batch submission whose file has several columns that could hold email addresses: the standard error envelope plus the candidate column names to choose between.| Field | Type | Description |
|---|---|---|
| status required | integer | HTTP status code mirrored in the body. example: 422 |
| code required | string |
Stable, machine-readable error code.
needs_column_selection
example: needs_column_selection
|
| error required | string | Human-readable error message. example: Several columns could hold email addresses. Re-send the request with email_column set to the one to validate. |
| candidate_columns required | array of string |
The columns to choose between, as 1-based positional tokens — They are positional rather than header names because a file may have no header row at
all, and the pipeline resolves the same tokens when it later reads the file. The
|
| doc_url | string | Link to the documentation for this error code. example: https://emailvalidator.ai/docs/get-started/errors |
| request_id | string | Correlation id for this request. example: 0HNCV1TQ3PLRK:00000003 |