Bulk email validation
Bulk validation processes JSON lists or CSV uploads in the background. Submit up to 10,000 email strings as JSON or 100,000 rows in a CSV using a write API token and an active subscription. Each completed address validation counts as one API request, including repeated addresses and addresses classified as unsafe. Malformed addresses and rows that fail validation after retries are not billed.
Use the dashboard
Section titled “Use the dashboard”Open Playground → Bulk Operations in your signed-in workspace. Choose CSV uploads to select a file, preview its columns, and choose the exact email column, or Pasted lists to enter one address per line. Domain checks are enabled by default in the dashboard; turn off Check MX and domain registration for faster processing.
Start verification to open the operation’s progress page. Active pages refresh through the shared dashboard polling system while the browser is visible, focused, and online. Results preserve row order and duplicate addresses. Use Inspect to view and copy an individual response, or download the completed CSV. CSV downloads preserve the original columns and include missing-result markers for partially completed operations.
Use Search this results page to filter the currently loaded results by email, original row number, verdict, or error text. Search ignores case and surrounding whitespace, stays active through refreshes and pagination, and leaves the full CSV download unchanged. Use Clear to restore the loaded page. Search CSV preview filters all columns in the first five preview rows; it does not search or modify the rest of the uploaded file. Both searches stay in your browser.
Recent operations combines CSV uploads and pasted lists in one table, newest first, including submissions made through the API. The Type column identifies each input format. Switching submission tabs keeps the shared history and its current page; each row opens its own results and CSV download. Existing operations remain accessible when new submissions are disabled. If a submission response was lost, Retry previous submission can recover accepted work using the unchanged input and original request identity, even while new submissions are disabled. The separate Playground → Email Verification page handles single addresses; existing /playground links continue to work.
After an uncertain submission failure, keep the page open and retry with the same input. The browser asks for confirmation before reloading while the attempt still needs recovery; staying on the page preserves the input and retry state.
Replacing an expired, unsubmitted CSV upload requires new submissions to be enabled. Retry with the same selected file and keep the page open so the original file and retry identity remain available.
Submit a list
Section titled “Submit a list”curl --request POST 'https://api.unwrap.email/v1/emails/bulk' \ --header "UNWRAP-EMAIL-API-TOKEN: $UNWRAP_EMAIL_API_TOKEN" \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: customer-import-2026-09-08' \ --data '{"emails":["person@example.com","invalid","person@example.com"],"validate_domain":1}'The response is 202 Accepted, with a job object, a status_url, and a matching Location header. Save the job ID. Processing starts asynchronously and does not depend on keeping the request or browser open.
Set validate_domain to 1 to include MX and registry checks. It defaults to 0, matching single-email validation. Existing normalization, blacklist checks, and contact-saving rules apply. Contact saving requires a safe address with valid MX records, so jobs without domain checks do not automatically populate contacts.
The request must contain between 1 and 10,000 strings and must fit within 5 MiB. Unsupported properties, non-string entries, and invalid settings reject the entire request. Empty or malformed email strings inside a valid list instead receive individual errors. After trimming, addresses must be at most 254 characters, with at most 64 characters before the @.
Use a distinct Idempotency-Key for each intended job. Retrying the same key and input returns the original job without starting additional work. Reusing a key with different input returns 409. Keys remain reserved until the job expires. Without a key, each submission creates a separate, billable job. You may have three unfinished jobs at a time; further submissions return 429.
Poll for progress and results
Section titled “Poll for progress and results”curl 'https://api.unwrap.email/v1/emails/bulk/JOB_ID?page=1&take=100' \ --header "UNWRAP-EMAIL-API-TOKEN: $UNWRAP_EMAIL_API_TOKEN"Use the returned status_url to poll every 10 seconds, backing off when requests fail. This single endpoint returns both progress and paginated results as { job, items, total, page, take }. The job contains status, total, processed, success, errors, created_at, updated_at, expires_at, and failure. The items array starts empty and fills as chunks finish. The top-level total counts committed result rows, while job.total counts all submitted addresses.
| Status | Meaning |
|---|---|
pending |
Inputs are saved and waiting for processing. |
processing |
Chunks are running or CSV output is being prepared. |
completed |
All rows have results and the CSV is ready. Individual rows may contain errors. |
failed |
Processing or CSV generation exhausted its retries. Committed JSON results remain available. |
expired |
The 30-day retrieval window has ended. Visible in job history until cleanup removes the job; polling returns 410. |
success counts completed validations, including unsafe addresses. Check each result’s validations.safe_to_send before deciding which addresses to use. Billing, analytics, and eligible contact updates are delivered asynchronously after results are persisted.
Each item has a zero-based index, the original input, and either a result matching single-email validation or an error with code and message. The unused field is null.
Whitespace is trimmed for validation while input preserves the supplied string. Every repeated occurrence keeps its own index, result ID, and usage unit.
Result pages contain committed rows in input order. While a job is processing, chunks may complete out of order and page membership can change. Wait for completed or failed before traversing a stable set of pages on this same endpoint. The default take is 25 and the maximum is 100. A failed job can have fewer committed results than submitted addresses. See the progress and results API reference for the response schema.
List previous jobs with GET /v1/emails/bulk?page=1&take=25. Bulk endpoints use standard API authentication. Read API tokens can retrieve owned job information and results under the existing token authentication rules; another user’s job returns 404.
Download CSV
Section titled “Download CSV”curl 'https://api.unwrap.email/v1/emails/bulk/JOB_ID/download' \ --header "UNWRAP-EMAIL-API-TOKEN: $UNWRAP_EMAIL_API_TOKEN"For a completed job, the response contains download_url and expires_at. Download using that URL as returned, preserving its complete query string. Links use the public CDN, expire after at most one hour, and cannot extend beyond the job’s expiry. Request a fresh link if an earlier one expires.
CSV preserves the original row order. Its first columns are index, input, error.code, and error.message, followed by available validation fields using dotted names. Arrays occupy one JSON-encoded cell. Empty cells represent absent values. Quotes, newlines, and spreadsheet formula prefixes are escaped for safe import.
Job data expires 30 days after submission. Expired polling and download requests return 410 until cleanup removes the job, after which requests return 404. Previously saved contacts follow their usual lifecycle.
See the bulk submission API reference for request and response schemas. Both JSON lists and CSV uploads use polling for progress. Cancellation and completion callbacks are not supported.
Upload a CSV
Section titled “Upload a CSV”CSV uploads support up to 100,000 data rows and 50 MiB. Request an upload URL with a write token and an active subscription:
curl --request POST 'https://api.unwrap.email/v1/emails/bulk/upload' \ --header "UNWRAP-EMAIL-API-TOKEN: $UNWRAP_EMAIL_API_TOKEN" \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: prepare-customers-2026-09' \ --data '{"filename":"customers.csv","size_bytes":12345}'Use the file’s exact byte length for size_bytes. The response includes upload_id, upload_url, required headers, and expires_at. PUT the file to that URL within 15 minutes, preserving the full URL and returned headers:
curl --request PUT "$UPLOAD_URL" \ --header 'Content-Type: text/csv' \ --header 'Content-Length: 12345' \ --header 'If-None-Match: *' \ --data-binary @customers.csvIn browsers, pass the file or Blob as the fetch body with Content-Type and If-None-Match; the browser sets Content-Length from the body. Upload requests need no cookies. The upload URL permits writing once; it cannot download or replace the source file.
Submit the upload within one hour to start validation:
curl --request POST 'https://api.unwrap.email/v1/emails/bulk' \ --header "UNWRAP-EMAIL-API-TOKEN: $UNWRAP_EMAIL_API_TOKEN" \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: validate-customers-2026-09' \ --data '{"upload_id":"UPLOAD_ID","email_column":"email","validate_domain":0}'Provide either emails or upload_id and email_column; mixed forms are rejected. validate_domain defaults to 0. The response is 202 with { group, status_url }. No separate upload-completion request is needed.
Use UTF-8, comma-separated CSV with unique, nonempty headers and an exact, case-sensitive email-column name. BOM, quoted commas, escaped quotes, multiline cells, and LF/CRLF are supported. Empty physical lines are ignored, while records containing empty cells are preserved. Empty datasets, malformed CSV, invalid UTF-8, inconsistent column counts, and records exceeding 64 KiB are rejected before validation starts. Blank or invalid email cells remain ordinary unbilled row errors.
Follow a CSV group
Section titled “Follow a CSV group”Poll the returned status_url, or call GET /v1/emails/bulk/groups/GROUP_ID. The response includes group counts, its processing phase, and child jobs with their individual status URLs. Phases are queued, parsing, processing, finalizing, completed, partial, failed, and expired. Expired group retrieval returns 410 until cleanup removes it.
A CSV is divided into jobs of up to 10,000 rows. The group counts as one of your three unfinished submissions, regardless of its child count. List groups with GET /v1/emails/bulk/groups?page=1&take=25; the existing job list continues to list standalone JSON submissions.
When download_ready is true, call GET /v1/emails/bulk/groups/GROUP_ID/download for one signed CSV link. The combined file preserves original columns, row order, duplicates, and empty cells. Validation columns are appended under unwrap., including a zero-based unwrap.index and error fields. If an original header uses that namespace, the export chooses the first unused numbered namespace, such as unwrap_1.. Check group.result_prefix for the selected prefix. Formula-like cells are protected with a leading apostrophe.
A partial group includes all original rows and marks unprocessed rows with job_failed. Ordinary validation errors still allow completed. Successful occurrences are billed individually; invalid rows and CSV generation are unbilled. Export retries do not repeat validation charges. Child jobs remain available through their normal polling and download endpoints.
One upload can belong to one group. Repeat an identical submission to retrieve that group; changed settings return 409. Preparation and submission idempotency keys are scoped separately. Matching preparation retries can refresh a signature within the original one-hour upload window. At most three unclaimed uploads are allowed. Accepted uploads and results are retained for 30 days from group creation; download links last at most one hour and never outlive that retention period.