Asynchronous REST API

Bulk verification API

Upload CSV or plain-text email lists directly to secure storage, submit asynchronous verification jobs, poll durable progress, and download enriched CSV results.

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.

BASE
https://api.mailtooth.com/verify/bulk
Direct uploads

Signed PUT targets keep files out of API memory and support CSV or one-address-per-line text input.

Durable progress

Poll status, processed and remaining counts, result buckets, warnings, and terminal failures.

Two result files

Download verified-only records or every input record with its verification outcome and enrichment.

Asynchronous by designPOST /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.

Supported authorization headersx-api-key: YOUR_API_KEYAuthorization: Bearer YOUR_API_KEY

Signed 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:

  1. Create an upload target and save its fileId, URL, and headers.
  2. PUT the exact file bytes to the signed storage URL.
  3. Submit the fileId as an asynchronous job and save its id.
  4. Poll the job until it reaches a terminal status.
  5. 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

InputRequirementNotes
CSVA 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 textOne email address per non-empty line. Do not send emailColumn.Each line becomes a record with an email column in the result CSV.
File size1 byte through 10 MiB (10,485,760 bytes).sizeBytes must equal the uploaded bytes, not a character count or estimate.
Content typetext/csv, application/csv, application/vnd.ms-excel, or text/plain.Use a CSV type or a .csv filename for CSV parsing.

Verification types

TypeOutcome represented in countsCost
quickDomain-level valid, invalid, or unknown. No mailbox contact.0.5 credit / record
standardDomain-level result plus extended domain intelligence.1 credit / record
deepcheckMailbox 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

POST
https://api.mailtooth.com/verify/bulk/uploads

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

FieldTypeRequiredDescription
fileNamestringYesOriginal filename, up to 255 characters.
contentTypestringYesA supported CSV or text MIME type.
sizeBytesintegerYesExact size from 1 through 10,485,760 bytes.
verificationTypestringNoquick, standard, or deepcheck. Defaults to standard.
emailColumnstringCSV onlyHeader containing email addresses, up to 255 characters.
Create upload · cURL
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"
  }'
Upload target · HTTP 201
{
  "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.

Upload bytes · cURL
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

POST
https://api.mailtooth.com/verify/bulk/jobs

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

FieldTypeRequiredDescription
fileIdUUIDYesThe fileId returned by POST /uploads for this account.
Submit job · cURL
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"}'
Queued job · HTTP 202
{
  "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"
}
Submission validates before queueingThe API streams through the source once to validate it and count records. Asynchronous verification starts after this request.

4. Track progress

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

Get job · cURL
curl 'https://api.mailtooth.com/verify/bulk/jobs/1f35fcb4-7ca0-4b2c-b582-e325b54636bb' \
  --header 'x-api-key: YOUR_API_KEY'
Processing job · HTTP 200
{
  "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

StatusMeaningClient action
queuedAccepted and waiting for a worker.Continue polling.
processingRecords are being verified.Continue polling.
stoppingA stop request is being settled.Continue polling.
stoppedProcessing stopped before the reservation was exhausted.Terminal. Download files when downloadsAvailable is true.
completedEvery source record was processed.Terminal. Download the result files.
partialAvailable credits covered only part of the source file.Terminal. Download processed results and inspect warning.
failedThe 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.

List jobs · cURL
curl 'https://api.mailtooth.com/verify/bulk/jobs' \
  --header 'Authorization: Bearer YOUR_API_KEY'

Job field reference

FieldTypeMeaning
idUUIDJob identifier for polling and downloads.
fileIdUUIDSource upload identifier.
fileName, emailColumnstring | nullOriginal filename and selected CSV email header.
statusstringCurrent lifecycle status.
verificationTypestringCanonical quick, standard, or deepcheck type.
totalRecordsintegerNon-empty records discovered during validation.
recordsReservedintegerRecords covered by this attempt's reservation.
processedRecordsintegerRecords processed by this attempt.
processedRecordsOverall, remainingRecords, progressPercentintegersOverall source progress, remainder, and percentage.
deliverableRecords, riskyRecords, undeliverableRecords, unknownRecordsintegersOutcome counts. For quick/standard, deliverable and undeliverable mean valid and invalid.
professionalRecords, personalRecords, unclassifiedRecordsintegersClassification totals for processed records.
reservedCredits, refundedCreditsnumbersReserved amount and terminal unknown/unused refund.
warning, failureReasonstring | nullNon-fatal guidance or customer-safe terminal failure.
downloadsAvailablebooleanWhether result links may be requested.
retryOfJobId, attemptNumber, retryStrategy, startRecord, resumeFromRecordmixedAttempt lineage and offsets. Normally null, 1, and 0 for a new API job.
canStop, canRetry, canResumebooleansLifecycle capabilities also consumed by the dashboard.
expiresAt, startedAt, completedAt, createdAt, updatedAtISO 8601Retention and lifecycle timestamps; nullable before events occur.

5. Download results

GET
https://api.mailtooth.com/verify/bulk/jobs/{jobId}/downloads

Request links only when downloadsAvailable is true. URLs expire after 15 minutes; call this endpoint again for fresh links.

Create download links · cURL
curl 'https://api.mailtooth.com/verify/bulk/jobs/1f35fcb4-7ca0-4b2c-b582-e325b54636bb/downloads' \
  --header 'x-api-key: YOUR_API_KEY'
Download links · HTTP 200
{
  "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"
}
Save result files · cURL
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.

FileIncluded recordsAppended result fields
*-verified.csvQuick/standard addressStatus valid; deepcheck mailbox deliverable.verification_score plus classification, role, person, and provider enrichment.
*-all-results.csvEvery processed record, including invalid, risky, undeliverable, and unknown.address_status, mailbox_status, risk_level, verification_score, and all enrichment.

Enrichment columns in both files

GroupColumns
Classificationemail_classification, email_classification_confidence, email_classification_source
Role-basedis_role_based, role_based_category, role_based_role, role_based_confidence
Personfirst_name, last_name, predicted_gender, gender_confidence
Provideremail_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.

EventCredit behavior
SubmissionReserves recordsReserved × the selected per-record price.
Known resultRetains the reserved per-record charge.
Unknown required resultRefunds that record automatically at settlement.
Unused reservationRefunds automatically after stop or failure settlement.

Read reservedCredits, refundedCredits, and warning rather than inferring billing from progress.

Errors and recovery

HTTPMeaningTypical cause
400
Invalid requestUnsupported metadata, malformed CSV, missing email column, size mismatch, expired file, or downloads requested too early.
401
UnauthorizedThe API key is missing, malformed, revoked, inactive, or belongs to a suspended account.
402
Insufficient creditsThe account cannot afford even one record at the selected verification type.
404
Not foundThe file or job does not exist, or it belongs to another account.
409
ConflictThe uploaded file already has a job or is already being processed.
500
Server errorStorage, queue publication, or another required operation failed unexpectedly.
Downloads unavailable · HTTP 400
{
  "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.
Every lookup enforces ownershipA resource created by one account cannot be submitted, viewed, or downloaded with another account's API key. Cross-account lookups return the same not-found behavior as missing resources.
Ready to process a list?

Create an API key and submit your first bulk job.

Open API keys