Docs · API reference
API reference
A small, versioned REST API. Bearer-key auth, optional idempotency, and a signed receipt on every verification. This page is kept in lock-step with the running service.
Base URL
https://verify.zi2.app
All paths under /v1
Auth
Bearer zi2v_live_…
Scopes: validate:read · validate:write
Format
JSON · UTF-8
Errors: { error: { code, message } }
Authentication
Send your key in the Authorization header as
Bearer zi2v_live_<prefix>_<secret>.
The secret is shown once at creation and stored only as a hash — mint and revoke keys under
Dashboard → API keys.
A key needs validate:write to submit work and
validate:read to poll and read results.
Idempotency
Send an Idempotency-Key header (8–128 chars, a UUID works well)
on POST requests. Retries with the same key return the original response and set
Idempotent-Replay: true. Reusing a key with a different body returns
409 CONFLICT. Keys are honored for 24 hours.
Endpoints
| Method | Path | Scope | Rate |
|---|---|---|---|
| POST | /v1/validate | validate:write | 500 / min |
| POST | /v1/batch | validate:write | 30 / min |
| GET | /v1/jobs/{id} | validate:read | — |
| GET | /v1/jobs/{id}/results.csv | validate:read | — |
| GET | /v1/balance | validate:read | — |
| GET | /v1/results | validate:read | 120 / min |
/v1/validate Verify a single email synchronously and get a signed receipt back.
Scope validate:write · Rate 500 / min · Cost 1 credit
| Field | Type | In | Notes |
|---|---|---|---|
| string | required | Address to verify (max 320 chars). | |
| cache | boolean | optional | Reuse this account’s cached probe for the address if present. Default false. |
Request · cURL
curl -X POST https://verify.zi2.app/v1/validate \
-H "Authorization: Bearer zi2v_live_abc12345_<secret>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 018f-a-uuid-v7-here" \
-d '{"email":"[email protected]"}' Response · 200 · also sets X-Credits-Remaining
{
"email": "[email protected]",
"status": "valid",
"score": 94,
"checks": {
"reason": "ok",
"syntax": true,
"mx": true,
"domain": "corp.io",
"mx_host": "mx1.corp.io",
"is_disposable": false,
"is_role_based": false,
"is_free_provider": false,
"is_catch_all": false,
"spf": "pass",
"dkim": "found",
"dmarc": "found",
"mta_sts": true,
"zi2_certified": false,
"auth": { "dmarc_policy": "reject", "dnssec": true, "...": "full AuthResult" }
},
"probed_at": "2026-07-15T09:41:53.000Z",
"latency_ms": 512,
"kid": "v1",
"sig": "MEUCIQD8f...q2b7Xn0A",
"credits_remaining": 2411,
"cached": false
} Note: kid and sig are top-level — the Ed25519 signature covers the canonical receipt (all keys sorted, minus sig) and verifies against the public JWKS.
/v1/batch Enqueue up to 100,000 emails for asynchronous validation. Credits are reserved at enqueue and settled at completion — unused rows are refunded.
Scope validate:write · Rate 30 / min · Cost N reserved
| Field | Type | In | Notes |
|---|---|---|---|
| emails | string[] | required | 1–100,000 addresses (each max 320). Lowercased + de-duplicated; you are charged the unique count. |
| webhookUrl | string (url) | optional | Called once when the job completes. |
| listName | string | optional | Label for the list (max 200). |
Request · cURL
curl -X POST https://verify.zi2.app/v1/batch \
-H "Authorization: Bearer zi2v_live_abc12345_<secret>" \
-H "Content-Type: application/json" \
-d '{
"emails": ["[email protected]", "[email protected]"],
"webhookUrl": "https://your.app/hooks/zi2v",
"listName": "Q3 outbound"
}' Response · 202
{
"job_id": "0f8b...c1",
"status": "queued",
"credits_reserved": 4200,
"progress": { "processed": 0, "total": 4200 },
"status_url": "/v1/jobs/0f8b...c1"
} Webhook · POST to your webhookUrl on completion
{
"event": "batch.completed",
"job_id": "0f8b...c1",
"processed": 4200,
"successful": 4187,
"completed_at": "2026-07-15T09:52:11.000Z"
} /v1/jobs/{id} Poll the status and progress of a batch job. results_url is populated once status is completed.
Scope validate:read · Rate — · Cost free
| Field | Type | In | Notes |
|---|---|---|---|
| id | string (uuid) | path | The job id returned by POST /v1/batch. |
Response · 200
{
"job_id": "0f8b...c1",
"status": "running",
"progress": { "processed": 1200, "total": 4200 },
"credits_reserved": 4200,
"created_at": "2026-07-15T09:40:00.000Z",
"started_at": "2026-07-15T09:40:03.000Z",
"completed_at": null,
"results_url": null
} /v1/jobs/{id}/results.csv Download completed batch results as CSV (attachment). Returns 400 until the job is completed.
Scope validate:read · Rate — · Cost free
| Field | Type | In | Notes |
|---|---|---|---|
| id | string (uuid) | path | The completed job id. |
CSV columns
email,status,score,is_disposable,is_role_based,is_catch_all,is_free_provider,mx_host,latency_ms,probed_at /v1/balance Current credit balance for the account.
Scope validate:read · Rate — · Cost free
Response · 200
{ "credits": 2411 } /v1/results Cursor-paginated validation history for the account — build your own dashboard against it.
Scope validate:read · Rate 120 / min · Cost free
| Field | Type | In | Notes |
|---|---|---|---|
| limit | integer | optional | Page size 1–200. Default 50. |
| cursor | string (ISO) | optional | probed_at of the last row of the previous page. |
| kind | single | batch | all | optional | Filter by source. Default all. |
Response · 200
{
"results": [
{ "id": "12345", "email": "[email protected]", "status": "valid", "score": 94,
"kind": "single", "job_id": null, "latency_ms": 512,
"probed_at": "2026-07-15T09:41:53.000Z", "cached_from": null }
],
"next_cursor": "2026-07-15T09:41:53.000Z"
} Verdicts
The status field is one of six values; score is an integer 0–100.
valid
Deliverable — mailbox accepts mail.
risky
Accept-all / low-confidence — deliver with care.
invalid
Undeliverable — rejected or no mailbox.
disposable
Throwaway / temporary provider.
unknown
Could not be determined (temp failure / timeout).
spamtrap
Known trap address — never send.
checks.reason gives the driver:
oksyntax_errordomain_not_foundno_mx_recordsmtp_rejectedmailbox_not_foundcatch_allrole_baseddisposable_domaintimeoutconnection_error
Batch status:
queuedrunningcompletedfailed
Rate limits
Fixed-window limits, keyed by source IP. Exceeding one returns 429 with a Retry-After header.
| POST /v1/validate | 500 / min |
| POST /v1/batch | 30 / min |
| GET /v1/results | 120 / min |
| All other endpoints | 300 / min (global) |
Errors
Every non-2xx response uses the same envelope: { "error": { "code", "message", "details"? } }.
| Code | HTTP | When |
|---|---|---|
| BAD_REQUEST | 400 | Validation failed, or insufficient credits (details.currentCredits). |
| UNAUTHORIZED | 401 | Missing, malformed, or revoked API key. |
| FORBIDDEN | 403 | API key lacks the required scope (details.missingScope). |
| NOT_FOUND | 404 | Job not found (or not owned by this account). |
| CONFLICT | 409 | Idempotency-Key reused with a different request body. |
| RATE_LIMITED | 429 | Too many requests — see the Retry-After header. |
| INTERNAL | 500 | Unexpected error. Safe to retry with the same Idempotency-Key. |
Build against the spec
Import the OpenAPI file into your client generator, or drop the Postman collection into your workspace.