Z2 ZI2 Verify
Start free

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
POST /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
email 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.

POST /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"
}
GET /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
}
GET /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
GET /v1/balance

Current credit balance for the account.

Scope validate:read · Rate — · Cost free

Response · 200

{ "credits": 2411 }
GET /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.