API reference
The VerifAi v1 REST API lets you verify a single address, enrich a contact record, read your credit balance, grade a list you manage (/v1/lists) and evaluate any list for free (/v1/evaluate) — each returns a single JSON object directly. Larger lists are handled asynchronously as jobs: see Bulk & async jobs for file uploads (/v1/files), batch validation (/v1/validate/batch) and job status (/v1/jobs). Every endpoint authenticates with a bearer API key.
Base URL
All paths below are relative to:
https://ratifai.app/hook/FlowClick/FlowVerify/v1POST/v1/validate
Verifies a single email address and returns a verdict with per-field provenance and an evidence trail.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The address to verify. |
depth | string | No | How deep to probe: syntax, mx_only or full. Defaults to full. Your plan’s maximum applies — a deeper request is degraded to what your plan allows and reported in depth_applied. |
idempotency_key | string | No | Makes the call safe to retry. May also be sent as the Idempotency-Key header, which wins if both are present. See Authentication. |
Response
| Field | Type | Description |
|---|---|---|
email | string | The address that was checked. |
verdict | string | deliverable, undeliverable, risky or unknown. |
reason | string | Machine-readable reason code for the verdict. |
depth_requested | string | The depth you asked for. |
depth_applied | string | The depth actually run after applying your plan limit. |
billable | boolean | Whether this result consumed a credit. |
credits_charged | integer | 0 or 1. See How billing works. |
checks | object | Per-field results: syntax, role_address, disposable, typo_suggestion, mx, smtp, catch_all. Each carries an origin (provenance). |
mail_provider | string | The identified mail provider, when known. |
enrichment | object | Provider classification and free_provider flag. |
evidence | array | Chronological record of what was observed, each with at, step and detail. |
request_id | string | Unique id for this request, also returned in the X-Request-Id header. |
Example
curl https://ratifai.app/hook/FlowClick/FlowVerify/v1/validate -H 'Authorization: Bearer YOUR_API_KEY' -H 'Content-Type: application/json' -d '{"email":"support@github.com","depth":"full"}'import requests
resp = requests.post(
"https://ratifai.app/hook/FlowClick/FlowVerify/v1/validate",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"email": "support@github.com", "depth": "full"},
)
print(resp.json())const resp = await fetch(
"https://ratifai.app/hook/FlowClick/FlowVerify/v1/validate",
{
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ email: "support@github.com", depth: "full" }),
},
);
const data = await resp.json();
console.log(data);{
"email": "support@github.com",
"verdict": "unknown",
"reason": "probe_failed_open",
"depth_requested": "full",
"depth_applied": "full",
"billable": false,
"credits_charged": 0,
"checks": {
"syntax": { "valid": true, "origin": "checked" },
"role_address": { "value": true, "origin": "checked" },
"disposable": { "value": false, "origin": "checked" },
"typo_suggestion": null,
"mx": { "records": ["github-com.mail.protection.outlook.com"], "origin": "checked" },
"smtp": { "accepted": false, "status": "unknown", "origin": "failed_open" },
"catch_all": { "value": false, "origin": "skipped" }
},
"mail_provider": "Microsoft 365",
"enrichment": {
"mail_provider": { "value": "Microsoft 365", "class": "cloud", "origin": "checked" },
"free_provider": { "value": false, "origin": "checked" }
},
"evidence": [
{ "at": "2026-08-05T18:02:40Z", "step": "syntax_check", "detail": "address is well-formed" },
{ "at": "2026-08-05T18:02:40Z", "step": "mx_lookup", "detail": "1 MX records for github.com" },
{ "at": "2026-08-05T18:02:40Z", "step": "smtp_probe", "detail": "mailbox probe result: unknown" }
],
"request_id": "req_1785952959868"
}POST/v1/enrich
Enriches and normalizes a contact record. Supply any subset of the fields below; VerifAi returns cleaned, standardized values with per-field provenance. Every returned field carries a sibling <field>_origin set to checked when it was derived from a supplied input, or skipped when its source input was absent. Enrichment is synchronous and charges nothing.
Request body
All fields are optional; send whichever you have.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | No | An email address to normalize and classify. |
name | string | No | A person’s name to split and canonicalize. |
phone | string | No | A phone number to normalize to E.164 and describe. |
address | string | No | A postal address to standardize. |
company | string | No | A company name to normalize. |
website | string | No | A website URL to normalize. |
Response
Each value field is accompanied by a <field>_origin sibling (checked or skipped). When an input is absent, its derived fields come back empty with _origin of skipped.
| Field | Type | Description |
|---|---|---|
normalized_email | string | The canonical form of the supplied email. |
email_domain | string | The domain part of the email. |
is_corporate | string | "true" or "false" — whether the email is a corporate (non-free) domain. |
name_first | string | Given name parsed from name. |
name_last | string | Family name parsed from name. |
name_canonical | string | The canonicalized full name. |
phone_e164 | string | The phone number in E.164 form. |
phone_national | string | The phone number in national format. |
phone_area_code | string | The area code. |
phone_timezone | string | IANA timezone inferred from the number. |
company_normalized | string | The normalized company name. |
company_legal_form | string | The legal form, e.g. Inc. |
address_standardized | string | The standardized postal address. |
website_normalized | string | The normalized website URL. |
completeness_score | string | A 0–100 score of how complete the record is. |
dedupe_key | string | A stable key for de-duplicating the record. |
<field>_origin | string | Provenance sibling for each field: checked or skipped. |
request_id | string | Unique id for this request. |
Example
curl https://ratifai.app/hook/FlowClick/FlowVerify/v1/enrich -H 'Authorization: Bearer YOUR_API_KEY' -H 'Content-Type: application/json' -d '{"email":"john.smith@acmecorp.com","name":"John Smith","phone":"+14155552671","company":"Acme Corporation"}'{
"normalized_email": "john.smith@acmecorp.com",
"normalized_email_origin": "checked",
"email_domain": "acmecorp.com",
"email_domain_origin": "checked",
"is_corporate": "true",
"is_corporate_origin": "checked",
"name_first": "John",
"name_first_origin": "checked",
"name_last": "Smith",
"name_last_origin": "checked",
"name_canonical": "John Smith",
"name_canonical_origin": "checked",
"phone_e164": "+14155552671",
"phone_e164_origin": "checked",
"phone_national": "(415) 555-2671",
"phone_national_origin": "checked",
"phone_area_code": "415",
"phone_area_code_origin": "checked",
"phone_timezone": "America/Los_Angeles",
"phone_timezone_origin": "checked",
"company_normalized": "Acme Corporation",
"company_normalized_origin": "checked",
"company_legal_form": "Inc",
"company_legal_form_origin": "checked",
"address_standardized": "",
"address_standardized_origin": "skipped",
"website_normalized": "",
"website_normalized_origin": "skipped",
"completeness_score": "75",
"completeness_score_origin": "checked",
"dedupe_key": "12f1f4a7...",
"dedupe_key_origin": "checked",
"request_id": "req_01J..."
}GET/v1/credits
Returns your current plan, credit balance and the start of the next billing period. Reading your balance is always free and never rate-limited beyond the standard per-key limit.
Response
| Field | Type | Description |
|---|---|---|
plan | string | Your current plan name. |
credits_balance | integer | Remaining monthly plan allowance. |
payg_credits | integer | Pay-as-you-go credits, which never expire. |
balance | integer | Total spendable credits (credits_balance + payg_credits). |
included_allowance | integer | Validations included with your plan each month. |
period | string | The current billing period, YYYY-MM. |
period_reset | string | Date the monthly allowance resets, YYYY-MM-DD. |
status | string | Account status, e.g. Active. |
request_id | string | Unique id for this request. |
Example
curl https://ratifai.app/hook/FlowClick/FlowVerify/v1/credits -H 'Authorization: Bearer YOUR_API_KEY'import requests
resp = requests.get(
"https://ratifai.app/hook/FlowClick/FlowVerify/v1/credits",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
print(resp.json())const resp = await fetch(
"https://ratifai.app/hook/FlowClick/FlowVerify/v1/credits",
{ headers: { "Authorization": "Bearer YOUR_API_KEY" } },
);
const data = await resp.json();
console.log(data);{
"plan": "Clean Pro",
"credits_balance": 24991,
"payg_credits": 0,
"balance": 24991,
"included_allowance": 25000,
"period": "2026-08",
"period_reset": "2026-09-01",
"status": "Active",
"request_id": "req_1785952911305"
}GET/v1/lists
Returns a health report for one of your lists — grade, verdict distribution, deliverable percentage, decay since the last check, and the enrichment lift we have added. Health is computed from validation results you already have, so it never re-validates a single address and never costs a credit. The list is identified by its id as a query parameter, the same convention as /v1/jobs (the webhook path exposes no trailing path segments). Requesting a list your key does not own returns 404 list_not_found — never a 403, which would confirm the id exists.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The list id, e.g. LST-00001. |
Grade bands
The letter grade maps the list’s deliverable percentage to A–F using published bands. The thresholds live in editable configuration (kv namespace fv_config, key grade_bands), not in code, so you can reproduce any grade from your own numbers:
| Grade | Deliverable % |
|---|---|
A | ≥ 90 |
B | ≥ 75 |
C | ≥ 60 |
D | ≥ 40 |
F | < 40 |
Response
| Field | Type | Description |
|---|---|---|
grade | string | The overall grade, A–F, from the bands above. |
total | integer | Records in the list. |
distribution | object | Counts by verdict: deliverable, undeliverable, risky, unknown. |
deliverable_pct | number | Deliverable records as a percentage of the total. |
decay | object | Change since the previous health check: since_last_check_days, newly_undeliverable, decay_pct. On a list checked only once it is { "status": "not_enough_history" } — decay needs a prior check to compare against. |
enrichment_lift | object | Field coverage before (as you supplied it) vs after enrichment, per family (phone, company, address, name_parsed), plus fields_added, records_improved and lift_pct. |
risks | array | Flags present in the list, any of disposable_domains, role_addresses, typo_domains. |
computed_at | string | When the report was computed. |
billable | boolean | Always false — health is free. |
request_id | string | Unique id for this request, also in X-Request-Id. |
Example
curl 'https://ratifai.app/hook/FlowClick/FlowVerify/v1/lists?id=LST-00001' -H 'Authorization: Bearer YOUR_API_KEY'{
"grade": "B",
"total": 9000,
"distribution": { "deliverable": 7412, "undeliverable": 981, "risky": 402, "unknown": 205 },
"deliverable_pct": 82.4,
"decay": { "since_last_check_days": 94, "newly_undeliverable": 143, "decay_pct": 1.6 },
"enrichment_lift": {
"before": { "phone": 1204, "company": 880, "address": 402, "name_parsed": 6100 },
"after": { "phone": 5840, "company": 7102, "address": 3990, "name_parsed": 9820 },
"fields_added": 18166,
"records_improved": 8104,
"lift_pct": 50.5
},
"risks": ["disposable_domains", "role_addresses", "typo_domains"],
"computed_at": "2026-08-06T00:00:00Z",
"billable": false,
"cost": "free - computing list health never uses credits",
"request_id": "req_1785974460465"
}POST/v1/evaluate
The free List Evaluator. Point it at a list of addresses and it tells you what is wrong with the list before you spend anything. It runs at syntax depth only — it makes no DNS or SMTP calls, writes nothing, and never costs a credit. It is the one endpoint you can call without an API key.
Because it is public it is rate limited. Called anonymously (no key) it is limited to 10 requests per minute per IP and a maximum of 5,000 addresses per request — over the cap returns 413 payload_too_large. Send a bearer key and your plan’s per-key limit applies instead and the size cap is lifted; for very large lists use /v1/files.
Request body
Send either a JSON object { "emails": [ ... ] } (or { "addresses": [ ... ] }), or a raw CSV / newline-delimited list — the address column is auto-detected and a header row is ignored.
Response
| Field | Type | Description |
|---|---|---|
total | integer | Addresses found in the body. |
unique | integer | Distinct addresses (case-insensitive). |
exact_duplicates | integer | total minus unique. |
syntactically_invalid | integer | Addresses that are not well-formed. |
disposable | integer | Addresses on known disposable / burner domains. |
role_addresses | integer | Role mailboxes such as info@, sales@, admin@. |
typo_domains | integer | Addresses on likely-mistyped domains. |
suggested_corrections | array | Per typo domain, the correction we would suggest: { domain, suggestion }. |
domain_distribution | array | The most common domains: { domain, count }, up to five. |
top_domain_concentration_pct | integer | Share of the list held by those top domains. |
grade | string | An overall grade using the same published bands as /v1/lists. |
quality_pct | number | The percentage the grade is based on. |
limitations | string | What this check cannot tell you — namely whether the mailboxes actually exist. |
upgrade | string | How to confirm deliverability (/v1/validate or /v1/files). |
billable | boolean | Always false. |
request_id | string | Unique id for this request. |
Example
curl https://ratifai.app/hook/FlowClick/FlowVerify/v1/evaluate -H 'Content-Type: application/json' -d '{"emails":["a@gmail.com","a@gmail.com","info@x.com","t@mailinator.com","z@gmial.com","bad@nodot"]}'{
"total": 6,
"unique": 5,
"exact_duplicates": 1,
"syntactically_invalid": 1,
"disposable": 1,
"role_addresses": 1,
"typo_domains": 1,
"grade": "D",
"quality_pct": 50.0,
"top_domain_concentration_pct": 100,
"domain_distribution": [{ "domain": "gmail.com", "count": 2 }, { "domain": "x.com", "count": 1 }, { "domain": "mailinator.com", "count": 1 }, { "domain": "gmial.com", "count": 1 }, { "domain": "nodot", "count": 1 }],
"suggested_corrections": [{ "domain": "gmial.com", "suggestion": "gmail.com" }],
"limitations": "This free evaluation checks address format and list hygiene only. It cannot tell you whether these mailboxes actually exist or accept mail - that requires a verification run.",
"upgrade": "To confirm deliverability, run these addresses through POST /v1/validate (single) or POST /v1/files (bulk), which probe MX and the SMTP mailbox.",
"depth": "syntax",
"billable": false,
"request_id": "req_1785974430429"
}Errors
Every error uses one envelope — { error: { code, message, doc_url }, request_id } — and its doc_url links to a stable anchor on the Errors page.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request was malformed. |
| 400 | file_empty | An uploaded file has no data rows. |
| 400 | batch_too_large | The emails array exceeds 10,000; use /v1/files. |
| 401 | invalid_api_key | Missing, malformed or expired key. |
| 401 | revoked_api_key | The key has been revoked. |
| 402 | insufficient_credits | No credits left for a billable check. |
| 404 | not_found | No such resource for your account. |
| 404 | job_not_found | No job with that id exists for your account. |
| 404 | list_not_found | No list with that id exists for your account. |
| 409 | job_not_ready | The job result is not ready yet; keep polling. |
| 413 | list_too_large | An uploaded file exceeds your plan’s list-size limit. |
| 413 | payload_too_large | The request body has too many addresses for /v1/evaluate; reduce it or use /v1/files. |
| 422 | no_email_column | No email column could be detected in an uploaded file. |
| 429 | rate_limited | Per-key rate limit exceeded; see Retry-After. |
| 500 | internal_error | Something failed on our side; safe to retry. A billable check that errors is not charged. |
Credits
You spend one credit per answer. Unknown results and syntax-depth checks are free. The full rules are on How billing works; read your live balance any time with /v1/credits. Bulk jobs are metered per billable row as they run.
This reference is also published as a machine-readable OpenAPI document.
Continuous hygiene — POST /v1/hygiene
Register a list for continuous hygiene, turn it off, or check its status. When a list is registered, VerifAi re-validates it automatically on your chosen cadence and sends a signed hygiene.decay webhook whenever addresses go bad, with the count, the addresses, and what it would have cost to send to them. A record is “under management” when it belongs to a list with an active hygiene registration; re-checking those records is free and unlimited. Registration itself costs no credits. Adding a new record to a registered list is a new record and meters normally.
Body: list_id (required), action = register | unregister | status (default status), cadence = 30|60|90 days (register only, default 30). Hygiene requires a paid plan; Clean allows one hygiene list up to 25,000 records, Clean Pro and above are unlimited in count up to their size cap. Over a limit returns 403 naming the limit and the upgrade; a list that is not yours returns 404.
curl -X POST https://ratifai.app/hook/FlowClick/FlowVerify/v1/hygiene \n -H "Authorization: Bearer $VERIFAI_KEY" \n -H "Content-Type: application/json" \n -d '{"list_id":"LST-00007","action":"register","cadence":"30"}'Returns the current state, e.g. {"list_id":"LST-00007","hygiene":"active","cadence_days":30,"next_run":"...","record_count":1234,"plan":"Clean Pro","under_management_free":true}
Email finder — POST /v1/find
Find a specific named person’s work email from their name and company domain. VerifAi determines the domain’s address pattern cheapest-source-first — its own verified corpus (addresses we have actually probed, not scraped guesses), then a learned-pattern cache, then generic ranked patterns (first.last, flast, first, f.last, firstlast, lastf, first_last) — and probes candidates over SMTP, stopping at the first deliverable. Probes per find are capped by your plan (5 by default).
Catch-all honesty. A catch-all domain accepts every address, so probing cannot confirm any one of them. VerifAi detects catch-all first and returns the most likely address with verified:false, a stated confidence and a reason — never a confident-looking address that bounces.
Bundled, not per hit. A find deducts zero credits. It is gated by plan (paid plans) and by the same per-key, per-minute rate limit shared across REST and MCP. The finder locates a named person’s address; it will not enumerate a domain, and sweeping many names hits the rate limit.
Body: first_name (required), last_name (required), and either domain or company (a company name is resolved to a domain, echoed back as domain_used). pattern_source is corpus, learned_cache or generic.
Example
curl https://ratifai.app/hook/FlowClick/FlowVerify/v1/find -H 'Authorization: Bearer YOUR_API_KEY' -H 'Content-Type: application/json' -d '{"first_name":"Dan","last_name":"Ludowise","domain":"example.com"}'A verified result — an address a mail server confirmed:
{
"email": "dan.ludowise@example.com",
"found": true,
"verified": true,
"confidence": 0.93,
"pattern": "first.last",
"pattern_source": "corpus",
"pattern_evidence_count": 41,
"candidates_probed": 1,
"alternatives": [
"dludowise@example.com"
],
"reason": "Pattern inferred from 41 previously verified addresses at this domain; candidate accepted by the mail server.",
"catch_all": false,
"corpus_hit": true,
"request_id": "req_01J...",
"domain_used": "example.com"
}On a catch-all domain, an honest best guess instead of a false positive:
{
"email": "jane.doe@example.com",
"found": true,
"verified": false,
"confidence": 0.35,
"pattern": "first.last",
"pattern_source": "generic",
"pattern_evidence_count": 0,
"candidates_probed": 0,
"alternatives": [],
"reason": "This domain is catch-all: it accepts all mail, so no address can be confirmed by probing. Returning the most likely address from generic evidence (unverified).",
"catch_all": true,
"corpus_hit": false,
"request_id": "req_01J...",
"domain_used": "example.com"
}A find that confirms nothing returns found:false with a reason — it does not return a guess dressed as a result. The full field reference is in the OpenAPI document.