Upload a CSV file and start a batch validation job

Download OpenAPI spec
POST /api/v1/verify/batch
API key required Base https://api.emailvalidator.ai 60 requests per minute per account

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

On this page

Parameters

header · parameters

ParameterTypeDescription
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

FieldTypeDescription
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

200
Job accepted and processing started. · application/json
202

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.

· application/json
400
Not multipart (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
401
Missing or invalid API key. · application/json
403
The API key authenticated but is not scoped for this endpoint (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
402
Insufficient credits. Body includes shortfall, balance, and required. · application/json
404

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.

· application/json
409
Job is not in pending state (concurrent submission conflict). · application/json
413
Upload exceeds the maximum allowed file size. · application/json
422
Ambiguous CSV — multiple candidate email columns detected. Re-POST with email_column set to the correct column name. · application/json
429
Rate limit exceeded. · application/json
500
Unexpected server error. The body carries code internal_error plus a request_id to quote in a support request, and never any exception detail. · application/json

Response schema · 200 · BatchStartResponse

Response from POST /api/v1/verify/batch when a job is accepted.
FieldTypeDescription
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

State of a batch job returned by GET /api/v1/verify/batch/{id}.
FieldTypeDescription
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

Standard error response shape used by all error status codes.
FieldTypeDescription
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

The 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.
FieldTypeDescription
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

The 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.
FieldTypeDescription
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 tokensColumn1 is the first column in the file, Column2 the second. Re-send the request with email_column set to one of these values.

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 email_column field additionally accepts the literal header cell text, if naming the column is more convenient.

example: ["Column1","Column2"]
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