Bulk & async jobs
VerifAi verifies large lists as asynchronous jobs. You submit the work, VerifAi runs it in the background, and you collect a single result CSV when it finishes. This page covers uploading a file, submitting a batch of addresses, checking job status, and downloading results.
Which calls are synchronous, which are jobs
Some endpoints answer immediately; others hand you a job to collect later.
| Endpoint | Mode | You get back |
|---|---|---|
| POST /v1/validate | Synchronous | The verdict, directly. See the API reference. |
| GET /v1/credits | Synchronous | Your balance, directly. |
| POST /v1/enrich | Synchronous | The enriched record, directly. See the API reference. |
| POST /v1/files | Asynchronous job | A 202 with a job handle. |
| POST /v1/validate/batch | Asynchronous job | A 202 with a job handle. |
A whole list cannot be verified within a single HTTP request, so file uploads and batches run as jobs. Single-address validation, credit reads and enrichment are cheap and answer inline.
The async model
Working with a job is three steps:
- Submit — upload a CSV to /v1/files or post a list to /v1/validate/batch. Both return
202with the same job handle:{ job_id, status, poll_url, created_at }. Thejob_idis VerifAi’s own opaque id (a short string such as"19"). - Wait — either poll GET /v1/jobs?id=<job_id> until
statusiscompleted, or (preferred) register a webhook and let VerifAi call you. Job webhooks firejob.completedwhen a job finishes andjob.failedif it fails, so you never have to poll. - Download — when the job is
completed, fetch the result CSV from theresult_urlthe job-status response gives you.
Read poll_url and result_url straight from the API responses — do not hand-build these URLs. The result lives on a sibling path (/v1result/job-result) and VerifAi returns the exact, ready-to-use URL in the result_url field once the job completes.
Base URL
All paths below are relative to:
https://ratifai.app/hook/FlowClick/FlowVerify/v1Every call authenticates with a bearer API key.
POST/v1/files
Uploads a CSV for verification and returns a job handle. Send the file as the raw request body with Content-Type: text/csv. If you do not supply a column mapping, VerifAi auto-detects the email column exactly the way the web uploader does: it first matches on header name (a column literally named email, e-mail, work email and the like), then falls back to inspecting values. Every non-email column is preserved and returned verbatim in the result CSV.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
depth | string | No | How deep to probe each row: syntax, mx_only or full. Defaults to full. Your plan’s maximum applies. |
list_name | string | No | A label for the list, shown in your dashboard. |
Request body
The raw CSV, sent with Content-Type: text/csv. The first row is treated as a header.
Response
Returns 202 Accepted with the job handle.
| Field | Type | Description |
|---|---|---|
job_id | string | VerifAi’s opaque job id. Pass it to /v1/jobs. |
status | string | Initial status, queued. |
poll_url | string | The exact URL to poll for status. Read it; do not build it. |
created_at | string | When the job was accepted, ISO 8601. |
Example
curl https://ratifai.app/hook/FlowClick/FlowVerify/v1/files?depth=full -H 'Authorization: Bearer YOUR_API_KEY' -H 'Content-Type: text/csv' --data-binary @contacts.csv{
"job_id": "19",
"status": "queued",
"poll_url": "https://ratifai.app/hook/FlowClick/FlowVerify/v1/jobs?id=19",
"created_at": "2026-08-05T21:45:20.000Z"
}Errors
| HTTP | Code | Meaning |
|---|---|---|
| 400 | file_empty | The uploaded file has no data rows. |
| 400 | invalid_request | A parameter was invalid, e.g. an unknown depth. |
| 422 | no_email_column | No email column could be detected. |
| 413 | list_too_large | The file exceeds your plan’s list-size limit. |
| 402 | insufficient_credits | Balance is already zero and the requested depth is billable. |
GET/v1/jobs
Returns the current status of a job. Pass the job_id you were given as the id query parameter. Poll this until status is completed — or skip polling entirely and use a job webhook.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The job_id returned when the job was submitted. |
Response
| Field | Type | Description |
|---|---|---|
job_id | string | The job’s opaque id. |
status | string | One of queued, running, completed or failed. |
progress | object | processed, total and percent. |
counts_by_verdict | object | Row counts per verdict once rows are processed. |
credits_charged | integer | Credits spent so far. It is normal and correct for this to be lower than progress.processed — it is the billing promise made visible, since only billable rows are charged. |
created_at | string | When the job was accepted. |
completed_at | string | When the job finished, or null until then. |
result_url | string | The exact URL to download the result CSV. It is null until status is completed. Read it; do not build it. |
request_id | string | Unique id for this request. |
Example
curl 'https://ratifai.app/hook/FlowClick/FlowVerify/v1/jobs?id=19' -H 'Authorization: Bearer YOUR_API_KEY'{
"job_id": "19",
"status": "completed",
"progress": { "processed": 1000, "total": 1000, "percent": 100 },
"counts_by_verdict": { "deliverable": 640, "undeliverable": 94, "risky": 45, "unknown": 221 },
"credits_charged": 779,
"created_at": "2026-08-05T21:45:20.000Z",
"completed_at": "2026-08-05T21:47:03.000Z",
"result_url": "https://ratifai.app/hook/FlowClick/FlowVerify/v1result/job-result?id=19",
"request_id": "req_01J..."
}Asking for a job id that is not yours returns 404 job_not_found — never 403, which would confirm the id exists.
Downloading the result
When status is completed, the job-status response carries a result_url. Fetch that URL as-is — VerifAi builds it for you; you should never construct the /v1result/job-result path by hand. The download returns one CSV (Content-Type: text/csv).
The result CSV mirrors the web download exactly: your original columns come first, in their original order, followed by the appended VerifAi columns. The full VerifAi column set is identical to the web export.
curl 'https://ratifai.app/hook/FlowClick/FlowVerify/v1result/job-result?id=19' -H 'Authorization: Bearer YOUR_API_KEY' -o results.csv"email","company","city","VerifAi Status","VerifAi Sub-Status","VerifAi Verdict","VerifAi Score"
"support@github.com","GitHub","San Francisco","Risky","Accept-All","risky","62"If you request the result before the job has finished, the download returns 409 job_not_ready as a JSON error envelope — never an empty file.
{
"error": {
"code": "job_not_ready",
"message": "The job has not finished yet. Poll the job status until it reports completed, then download the result.",
"doc_url": "https://ratifai.app/docs-errors.html#job_not_ready"
},
"request_id": "req_01J..."
}POST/v1/validate/batch
Verifies a list of addresses supplied inline as JSON. Like /v1/files, this now runs as a job: it returns 202 with the same { job_id, status, poll_url, created_at } handle, and you poll and download it in exactly the same way.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
emails | array | Yes | The addresses to verify. Up to 10,000 per batch. An empty array is rejected. |
depth | string | No | syntax, mx_only or full. Defaults to full; your plan’s maximum applies. |
idempotency_key | string | No | Makes the submission safe to retry. May also be sent as the Idempotency-Key header, which wins if both are present. |
Response
Returns 202 Accepted with the same job handle as /v1/files: job_id, status, poll_url and created_at.
Example
curl https://ratifai.app/hook/FlowClick/FlowVerify/v1/validate/batch -H 'Authorization: Bearer YOUR_API_KEY' -H 'Content-Type: application/json' -d '{"emails":["support@github.com","hi@example.com"],"depth":"full"}'{
"job_id": "20",
"status": "queued",
"poll_url": "https://ratifai.app/hook/FlowClick/FlowVerify/v1/jobs?id=20",
"created_at": "2026-08-05T21:46:00.000Z"
}Limits
A batch may carry at most 10,000 addresses. Above that, the call returns 400 batch_too_large and points you at POST /v1/files for larger lists. An empty emails array returns 400 invalid_request.
End-to-end walkthrough
Upload a file, wait for it to finish, then download the CSV. These commands run exactly as printed.
# 1. Submit the file. The 202 response carries the job_id.
curl https://ratifai.app/hook/FlowClick/FlowVerify/v1/files -H 'Authorization: Bearer YOUR_API_KEY' -H 'Content-Type: text/csv' --data-binary @contacts.csv
# 2. Poll until status is completed. Read result_url and poll_url from the
# response body, never hand-build them.
curl 'https://ratifai.app/hook/FlowClick/FlowVerify/v1/jobs?id=19' -H 'Authorization: Bearer YOUR_API_KEY'
# 3. Once completed, download the result CSV from result_url.
curl 'https://ratifai.app/hook/FlowClick/FlowVerify/v1result/job-result?id=19' -H 'Authorization: Bearer YOUR_API_KEY' -o results.csvPrefer not to poll? Register a webhook instead and act on the job.completed event when it arrives; handle job.failed for jobs that do not finish.
Credits for jobs
Jobs are admitted and metered per row as they run. You are charged only for billable rows. Unknown results, syntax-depth checks, and rows that are suppressed, failed-open, throttled or skipped are free, and re-checks of records already under management are free. This is why credits_charged is expected to be lower than progress.processed.
If your balance reaches zero part-way through a run, the remaining billable rows are returned as unknown and are not charged — the job still completes. Top up and re-run; re-checks are free. A job is only refused up front with 402 insufficient_credits when your balance is already zero and the requested depth is billable. The full rules are on How billing works; read your live balance any time with /v1/credits.