Overview
Bulk verification is a four-stage workflow: reserve an upload target, upload the file, submit a job, then poll until result files are ready. Files bypass the MailTooth API process and stream directly to object storage, while the API remains the source of truth for ownership, credits, status, progress, and downloads.
https://api.mailtooth.com/verify/bulkSigned PUT targets keep files out of API memory and support CSV or one-address-per-line text input.
Poll status, processed and remaining counts, result buckets, warnings, and terminal failures.
Download verified-only records or every input record with its verification outcome and enrichment.
POST /jobs returns HTTP 202 after the job is queued. It does not wait for verification to finish. Save the returned job id and poll its resource.The developer API currently exposes submission, listing, polling, and downloads. Stop, restart, and resume controls remain dashboard operations, although their state can appear in job responses.
Authentication
Send an active developer API key to every MailTooth API endpoint. Use either x-api-key or a Bearer token. Keep the key in a server-side secret store; never place it in a browser bundle, source repository, uploaded file, or signed-storage request.
x-api-key: YOUR_API_KEYAuthorization: Bearer YOUR_API_KEYSigned upload and download URLs are temporary credentials of their own. Do not attach your MailTooth API key when calling those URLs.
End-to-end flow
Complete these requests in order:
- Create an upload target and save its fileId, URL, and headers.
- PUT the exact file bytes to the signed storage URL.
- Submit the fileId as an asynchronous job and save its id.
- Poll the job until it reaches a terminal status.
- Request fresh download links and save either or both CSV files.
Do not submit the job until the storage PUT returns a successful 2xx response. Uploading and job submission are separate operations.
Input file requirements
| Input | Requirement | Notes |
|---|---|---|
| CSV | A header row and an emailColumn value that identifies the email header after surrounding whitespace is trimmed. | UTF-8 BOM, quoted fields, empty lines, and additional columns are supported. Original columns are preserved. |
| Plain text | One email address per non-empty line. Do not send emailColumn. | Each line becomes a record with an email column in the result CSV. |
| File size | 1 byte through 10 MiB (10,485,760 bytes). | sizeBytes must equal the uploaded bytes, not a character count or estimate. |
| Content type | text/csv, application/csv, application/vnd.ms-excel, or text/plain. | Use a CSV type or a .csv filename for CSV parsing. |
Verification types
| Type | Outcome represented in counts | Cost |
|---|---|---|
quick | Domain-level valid, invalid, or unknown. No mailbox contact. | 0.5 credit / record |
standard | Domain-level result plus extended domain intelligence. | 1 credit / record |
deepcheck | Mailbox deliverable, risky, undeliverable, or unknown. | 2 credits / record |
standard is the default. Deprecated enriched and pro inputs map to deepcheck.
1. Create an upload target
https://api.mailtooth.com/verify/bulk/uploadsDescribe the file before uploading it. For CSV, emailColumn is required. For plain text it must be omitted. The response contains a file ID plus a signed PUT target.
| Field | Type | Required | Description |
|---|---|---|---|
fileName | string | Yes | Original filename, up to 255 characters. |
contentType | string | Yes | A supported CSV or text MIME type. |
sizeBytes | integer | Yes | Exact size from 1 through 10,485,760 bytes. |
verificationType | string | No | quick, standard, or deepcheck. Defaults to standard. |
emailColumn | string | CSV only | Header containing email addresses, up to 255 characters. |
curl --request POST 'https://api.mailtooth.com/verify/bulk/uploads' \
--header 'Content-Type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '{
"fileName": "contacts.csv",
"contentType": "text/csv",
"sizeBytes": 128,
"verificationType": "standard",
"emailColumn": "email"
}'{
"fileId": "2d66fc31-73c2-4adb-9f22-ec78c7097198",
"uploadUrl": "https://storage.example.com/bulk/...signed-query...",
"headers": {
"Content-Type": "text/csv",
"Content-Length": "128"
},
"expiresAt": "2026-09-13T08:30:00.000Z",
"maxFileSizeBytes": 10485760
}The upload URL lasts 15 minutes. Response expiresAt is the 30-day file-retention deadline, not the upload URL expiry.
2. Upload the file
PUT the original bytes to uploadUrl with every returned header. This request goes to object storage and does not use your MailTooth API key.
curl --request PUT 'UPLOAD_URL_FROM_RESPONSE' \
--header 'Content-Type: text/csv' \
--header 'Content-Length: 128' \
--upload-file './contacts.csv'- Do not change line endings, reserialize CSV, or compress after calculating sizeBytes.
- Keep Content-Type and Content-Length identical to the returned headers.
- Treat uploadUrl as a secret and discard it after the PUT succeeds.
- If it expires before upload succeeds, create a new upload target.
3. Submit the job
https://api.mailtooth.com/verify/bulk/jobsSubmit the uploaded fileId. The API confirms stored size, validates and counts records, reserves available credits atomically, creates the durable job, and publishes it to the queue.
| Field | Type | Required | Description |
|---|---|---|---|
fileId | UUID | Yes | The fileId returned by POST /uploads for this account. |
curl --request POST 'https://api.mailtooth.com/verify/bulk/jobs' \
--header 'Content-Type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '{"fileId":"2d66fc31-73c2-4adb-9f22-ec78c7097198"}'{
"id": "1f35fcb4-7ca0-4b2c-b582-e325b54636bb",
"fileId": "2d66fc31-73c2-4adb-9f22-ec78c7097198",
"retryOfJobId": null,
"attemptNumber": 1,
"retryStrategy": null,
"startRecord": 0,
"fileName": "contacts.csv",
"emailColumn": "email",
"status": "queued",
"totalRecords": 3,
"verificationType": "standard",
"recordsReserved": 3,
"reservedCredits": 3,
"refundedCredits": 0,
"processedRecords": 0,
"processedRecordsOverall": 0,
"remainingRecords": 3,
"deliverableRecords": 0,
"riskyRecords": 0,
"undeliverableRecords": 0,
"unknownRecords": 0,
"professionalRecords": 0,
"personalRecords": 0,
"unclassifiedRecords": 0,
"progressPercent": 0,
"warning": null,
"failureReason": null,
"downloadsAvailable": false,
"canStop": true,
"canRetry": false,
"canResume": false,
"resumeFromRecord": 0,
"expiresAt": "2026-09-13T08:30:00.000Z",
"startedAt": null,
"completedAt": null,
"createdAt": "2026-08-14T08:31:00.000Z",
"updatedAt": "2026-08-14T08:31:00.000Z"
}4. Track progress
https://api.mailtooth.com/verify/bulk/jobs/{jobId}Poll with the submitted job ID. Progress is checkpointed, so counts may advance in batches rather than after every email.
curl 'https://api.mailtooth.com/verify/bulk/jobs/1f35fcb4-7ca0-4b2c-b582-e325b54636bb' \
--header 'x-api-key: YOUR_API_KEY'{
"id": "1f35fcb4-7ca0-4b2c-b582-e325b54636bb",
"fileId": "2d66fc31-73c2-4adb-9f22-ec78c7097198",
"retryOfJobId": null,
"attemptNumber": 1,
"retryStrategy": null,
"startRecord": 0,
"fileName": "contacts.csv",
"emailColumn": "email",
"status": "processing",
"totalRecords": 3,
"verificationType": "standard",
"recordsReserved": 3,
"reservedCredits": 3,
"refundedCredits": 0,
"processedRecords": 2,
"processedRecordsOverall": 2,
"remainingRecords": 1,
"deliverableRecords": 1,
"riskyRecords": 0,
"undeliverableRecords": 1,
"unknownRecords": 0,
"professionalRecords": 1,
"personalRecords": 1,
"unclassifiedRecords": 0,
"progressPercent": 67,
"warning": null,
"failureReason": null,
"downloadsAvailable": false,
"canStop": true,
"canRetry": false,
"canResume": false,
"resumeFromRecord": 2,
"expiresAt": "2026-09-13T08:30:00.000Z",
"startedAt": "2026-08-14T08:31:01.000Z",
"completedAt": null,
"createdAt": "2026-08-14T08:31:00.000Z",
"updatedAt": "2026-08-14T08:31:05.000Z"
}Lifecycle statuses
| Status | Meaning | Client action |
|---|---|---|
queued | Accepted and waiting for a worker. | Continue polling. |
processing | Records are being verified. | Continue polling. |
stopping | A stop request is being settled. | Continue polling. |
stopped | Processing stopped before the reservation was exhausted. | Terminal. Download files when downloadsAvailable is true. |
completed | Every source record was processed. | Terminal. Download the result files. |
partial | Available credits covered only part of the source file. | Terminal. Download processed results and inspect warning. |
failed | The queue or verifier could not finish the job. | Terminal. Inspect failureReason; result downloads are unavailable. |
List recent jobs
GET /jobs returns up to 100 jobs for the API-key account, newest first. Every item has the same job shape.
curl 'https://api.mailtooth.com/verify/bulk/jobs' \
--header 'Authorization: Bearer YOUR_API_KEY'Job field reference
| Field | Type | Meaning |
|---|---|---|
id | UUID | Job identifier for polling and downloads. |
fileId | UUID | Source upload identifier. |
fileName, emailColumn | string | null | Original filename and selected CSV email header. |
status | string | Current lifecycle status. |
verificationType | string | Canonical quick, standard, or deepcheck type. |
totalRecords | integer | Non-empty records discovered during validation. |
recordsReserved | integer | Records covered by this attempt's reservation. |
processedRecords | integer | Records processed by this attempt. |
processedRecordsOverall, remainingRecords, progressPercent | integers | Overall source progress, remainder, and percentage. |
deliverableRecords, riskyRecords, undeliverableRecords, unknownRecords | integers | Outcome counts. For quick/standard, deliverable and undeliverable mean valid and invalid. |
professionalRecords, personalRecords, unclassifiedRecords | integers | Classification totals for processed records. |
reservedCredits, refundedCredits | numbers | Reserved amount and terminal unknown/unused refund. |
warning, failureReason | string | null | Non-fatal guidance or customer-safe terminal failure. |
downloadsAvailable | boolean | Whether result links may be requested. |
retryOfJobId, attemptNumber, retryStrategy, startRecord, resumeFromRecord | mixed | Attempt lineage and offsets. Normally null, 1, and 0 for a new API job. |
canStop, canRetry, canResume | booleans | Lifecycle capabilities also consumed by the dashboard. |
expiresAt, startedAt, completedAt, createdAt, updatedAt | ISO 8601 | Retention and lifecycle timestamps; nullable before events occur. |
5. Download results
https://api.mailtooth.com/verify/bulk/jobs/{jobId}/downloadsRequest links only when downloadsAvailable is true. URLs expire after 15 minutes; call this endpoint again for fresh links.
curl 'https://api.mailtooth.com/verify/bulk/jobs/1f35fcb4-7ca0-4b2c-b582-e325b54636bb/downloads' \
--header 'x-api-key: YOUR_API_KEY'{
"expiresAt": "2026-09-13T08:30:00.000Z",
"verifiedRecordsUrl": "https://storage.example.com/.../verified.csv?signed-query",
"allResultsUrl": "https://storage.example.com/.../all-results.csv?signed-query"
}curl --location 'VERIFIED_RECORDS_URL' \
--output './contacts-verified.csv'
curl --location 'ALL_RESULTS_URL' \
--output './contacts-all-results.csv'Response expiresAt is the result-retention deadline, separate from each signed URL's shorter lifetime.
Result file contents
Both CSV exports preserve every input column in its original order, then append normalized verification and enrichment columns.
| File | Included records | Appended result fields |
|---|---|---|
*-verified.csv | Quick/standard addressStatus valid; deepcheck mailbox deliverable. | verification_score plus classification, role, person, and provider enrichment. |
*-all-results.csv | Every processed record, including invalid, risky, undeliverable, and unknown. | address_status, mailbox_status, risk_level, verification_score, and all enrichment. |
Enrichment columns in both files
| Group | Columns |
|---|---|
| Classification | email_classification, email_classification_confidence, email_classification_source |
| Role-based | is_role_based, role_based_category, role_based_role, role_based_confidence |
| Person | first_name, last_name, predicted_gender, gender_confidence |
| Provider | email_provider_id, email_provider, email_infrastructure_type, email_provider_routing_category, email_provider_confidence, observed_email_provider_ids, has_secondary_email_provider, has_unrecognized_mx |
verification_score is blank when the selected tier does not produce a score. Missing enrichment uses an empty cell or its documented unknown fallback.
Credits and partial jobs
Submission reserves credits before queueing. If the account can afford at least one record but not the full file, the API reserves as many as possible, returns a warning, and finishes as partial. If it cannot afford one record, submission returns HTTP 402 and creates no job.
| Event | Credit behavior |
|---|---|
| Submission | Reserves recordsReserved × the selected per-record price. |
| Known result | Retains the reserved per-record charge. |
| Unknown required result | Refunds that record automatically at settlement. |
| Unused reservation | Refunds automatically after stop or failure settlement. |
Read reservedCredits, refundedCredits, and warning rather than inferring billing from progress.
Errors and recovery
| HTTP | Meaning | Typical cause |
|---|---|---|
400 | Invalid request | Unsupported metadata, malformed CSV, missing email column, size mismatch, expired file, or downloads requested too early. |
401 | Unauthorized | The API key is missing, malformed, revoked, inactive, or belongs to a suspended account. |
402 | Insufficient credits | The account cannot afford even one record at the selected verification type. |
404 | Not found | The file or job does not exist, or it belongs to another account. |
409 | Conflict | The uploaded file already has a job or is already being processed. |
500 | Server error | Storage, queue publication, or another required operation failed unexpectedly. |
{
"statusCode": 400,
"message": "Result files are not available.",
"error": "Bad Request"
}- Fix 400 responses before retrying the request.
- Treat 401 and 402 as configuration or account-state failures.
- After a submission timeout, list recent jobs before retrying POST /jobs; the original may have succeeded.
- Retry unexpected 500 responses with exponential backoff and jitter.
Production integration guidance
- Persist fileId until submission and job id through the retention window.
- Poll every 2–5 seconds initially, then back off for long jobs.
- Stop polling only on completed, partial, stopped, or failed.
- Use downloadsAvailable—not status alone—to request result links.
- Generate download links just in time and redact them from logs.
- Read CSV headers by name instead of relying on appended positions.
- Surface warning and failureReason to operators.
- Download before expiresAt; files are retained for 30 days.