{
  "openapi": "3.0.3",
  "info": {
    "title": "VerifAi API",
    "version": "1.0.0",
    "description": "Email verification and contact-data quality. Verify a single address, enrich a contact record, and read your credit balance synchronously; verify large lists asynchronously as jobs (upload a CSV or submit a batch, poll job status or use a webhook, then download the result CSV). Authenticate with a bearer API key created in the VerifAi dashboard. This document is maintained by hand alongside the endpoints; when an endpoint changes, update this file."
  },
  "servers": [
    {
      "url": "https://ratifai.app/hook/FlowClick/FlowVerify/v1",
      "description": "VerifAi v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/validate": {
      "post": {
        "operationId": "validateEmail",
        "summary": "Verify a single email address",
        "description": "Returns a verdict with per-field provenance and an evidence trail. Depth may be degraded to your plan's maximum, reported in depth_applied. You are charged one credit only for a definitive answer; unknown results and syntax-depth checks are free.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ValidateRequest"
              },
              "example": {
                "email": "support@github.com",
                "depth": "full"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Makes the call safe to retry. Wins over an idempotency_key body field if both are present."
          }
        ],
        "responses": {
          "200": {
            "description": "Verification result.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "Unique id for this request."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateResponse"
                },
                "example": {
                  "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",
                  "quality_score": {
                    "available": true,
                    "score": 38,
                    "band": "F",
                    "factors": [
                      {
                        "factor": "verdict_risky",
                        "direction": "+",
                        "weight": 45,
                        "detail": "Delivery is uncertain - a risky verdict such as catch-all."
                      },
                      {
                        "factor": "corporate_domain",
                        "direction": "+",
                        "weight": 15,
                        "detail": "Hosted on a corporate mail domain, not a free consumer provider."
                      },
                      {
                        "factor": "role_address",
                        "direction": "-",
                        "weight": -12,
                        "detail": "Addressed to a role such as info@ or sales@, not a named person."
                      },
                      {
                        "factor": "catch_all_domain",
                        "direction": "-",
                        "weight": -10,
                        "detail": "The domain accepts all mail, so this specific mailbox could not be confirmed to exist."
                      }
                    ],
                    "explanation": "Scored 38 out of 100 (band F) - a low-quality address to send to."
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredits"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/enrich": {
      "post": {
        "operationId": "enrichContact",
        "summary": "Enrich and normalize a contact record",
        "description": "Accepts any subset of email, name, phone, address, company and website, and returns cleaned, standardized values. Every returned field carries a sibling <field>_origin set to checked (derived from a supplied input) or skipped (its source input was absent). Synchronous and free of charge.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EnrichRequest"
              },
              "example": {
                "email": "john.smith@acmecorp.com",
                "name": "John Smith",
                "phone": "+14155552671",
                "company": "Acme Corporation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Enriched record with per-field provenance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnrichResponse"
                },
                "example": {
                  "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..."
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/files": {
      "post": {
        "operationId": "uploadFile",
        "summary": "Upload a CSV for asynchronous verification",
        "description": "Uploads a CSV as the raw request body (Content-Type text/csv) and returns a 202 job handle. If no column mapping is supplied, the email column is auto-detected the same way the web uploader does (header-name match, then a value-based fallback). Every non-email column is preserved and returned verbatim in the result CSV. Poll GET /jobs or register a job.completed webhook, then download the result from result_url. Jobs are metered per billable row as they run.",
        "parameters": [
          {
            "name": "depth",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "syntax",
                "mx_only",
                "full"
              ],
              "default": "full"
            },
            "description": "How deep to probe each row. Degraded to your plan maximum."
          },
          {
            "name": "list_name",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "A label for the list, shown in your dashboard."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "text/csv": {
              "schema": {
                "type": "string",
                "format": "binary",
                "description": "The raw CSV; first row is treated as a header."
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobHandle"
                },
                "example": {
                  "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"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/FileEmpty"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredits"
          },
          "413": {
            "$ref": "#/components/responses/ListTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/NoEmailColumn"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/validate/batch": {
      "post": {
        "operationId": "validateBatch",
        "summary": "Verify a list of addresses asynchronously",
        "description": "Accepts up to 10,000 addresses inline and returns a 202 job handle with the same shape as POST /files. Poll GET /jobs or use a webhook, then download the result from result_url. Above 10,000 addresses the call is rejected with batch_too_large; use POST /files for larger lists. An empty emails array is rejected with invalid_request.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchRequest"
              },
              "example": {
                "emails": [
                  "support@github.com",
                  "hi@example.com"
                ],
                "depth": "full"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Makes the submission safe to retry. Wins over an idempotency_key body field if both are present."
          }
        ],
        "responses": {
          "202": {
            "description": "Job accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobHandle"
                },
                "example": {
                  "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"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BatchTooLarge"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredits"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/jobs": {
      "get": {
        "operationId": "getJob",
        "summary": "Read the status of an asynchronous job",
        "description": "Returns the current status of a job created by POST /files or POST /validate/batch. Pass the opaque job_id as the id query parameter. Poll until status is completed, or register a job.completed / job.failed webhook instead. credits_charged is expected to be lower than progress.processed because only billable rows are charged. When completed, download the result CSV from result_url (do not hand-build it). Requesting another account's job id returns job_not_found, never 403.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The opaque job_id returned when the job was submitted."
          }
        ],
        "responses": {
          "200": {
            "description": "Current job status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatus"
                },
                "example": {
                  "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..."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/JobNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/jobs/result": {
      "get": {
        "operationId": "downloadJobResult",
        "summary": "Download the result CSV of a completed job",
        "description": "The result download is NOT under the v1 base path. Read the absolute result_url from the job-status response and fetch it as-is; do not construct the URL yourself. It resolves to the sibling path https://ratifai.app/hook/FlowClick/FlowVerify/v1result/job-result?id=<JOB_ID>. Returns one CSV: your original columns first, in their original order, with the VerifAi columns appended, identical to the web export. Before the job is complete it returns 409 job_not_ready as a JSON error envelope, never an empty file. This path item documents that download for reference only; the real URL comes from result_url.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The opaque job_id. In practice this URL is supplied ready-built in the result_url field of the job status."
          }
        ],
        "responses": {
          "200": {
            "description": "The result CSV.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                },
                "example": "\"email\",\"company\",\"city\",\"VerifAi Status\",\"VerifAi Sub-Status\",\"VerifAi Verdict\",\"VerifAi Score\"\n\"support@github.com\",\"GitHub\",\"San Francisco\",\"Risky\",\"Accept-All\",\"risky\",\"62\"\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/JobNotFound"
          },
          "409": {
            "$ref": "#/components/responses/JobNotReady"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/credits": {
      "get": {
        "operationId": "getCredits",
        "summary": "Read your credit balance",
        "description": "Returns your plan, credit balance and the start of the next billing period. Always free.",
        "responses": {
          "200": {
            "description": "Current balance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreditsResponse"
                },
                "example": {
                  "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"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/lists": {
      "get": {
        "operationId": "getListHealth",
        "summary": "Health report for a managed list",
        "description": "Returns grade, verdict distribution, deliverable percentage, decay and enrichment lift for a list your key owns. Computed from validation results you already have - it never re-validates and never costs a credit. Identify the list by its id query parameter (the webhook path exposes no trailing segments, the same convention as GET /jobs). Another tenant's list returns 404 list_not_found, never 403.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The list id, e.g. LST-00001."
          }
        ],
        "responses": {
          "200": {
            "description": "List health report.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "Unique id for this request."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListHealthResponse"
                },
                "example": {
                  "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"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/ListNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/evaluate": {
      "post": {
        "operationId": "evaluateList",
        "summary": "Free List Evaluator (syntax depth, no credits)",
        "description": "Assesses a list's quality without validating it. Runs at syntax depth only - no DNS or SMTP, no credits, ever. It is the only endpoint callable without an API key. Called anonymously it is limited to 10 requests per minute per IP and a maximum of 5,000 addresses (over the cap returns 413 payload_too_large); send a bearer key to lift the size cap and apply your per-key limit instead.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EvaluateRequest"
              },
              "example": {
                "emails": [
                  "a@gmail.com",
                  "a@gmail.com",
                  "info@x.com",
                  "t@mailinator.com",
                  "z@gmial.com",
                  "bad@nodot"
                ]
              }
            },
            "text/csv": {
              "schema": {
                "type": "string",
                "description": "A raw CSV or newline-delimited list; the address column is auto-detected and a header row is ignored."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "List quality assessment.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "Unique id for this request."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvaluateResponse"
                },
                "example": {
                  "total": 6,
                  "unique": 5,
                  "exact_duplicates": 1,
                  "syntactically_invalid": 1,
                  "disposable": 1,
                  "role_addresses": 1,
                  "typo_domains": 1,
                  "grade": "D",
                  "quality_pct": 50,
                  "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"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/dedupe": {
      "post": {
        "operationId": "dedupeRecords",
        "summary": "Propose duplicate clusters and golden records (never applies a merge)",
        "description": "Deduplicates a set of inline records or a managed list and returns proposed clusters with a golden record each, using fuzzy matching on normalized values. Returns proposals only — it never modifies, deletes or merges your source records, and deducts no credits. Inline arrays over 1000 records return 413; a large managed list returns 202 with a job handle polled at /jobs, and the same signed job.completed webhook fires.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DedupeRequest"
              },
              "example": {
                "records": [
                  {
                    "email": "pat@acme.com",
                    "name": "Pat Lee",
                    "company": "Acme"
                  },
                  {
                    "email": "pat@acme.com",
                    "name": "Patrick Lee",
                    "company": "Acme Inc"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Proposed clusters and a summary. Nothing was applied to your records.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DedupeResponse"
                },
                "example": {
                  "clusters": [
                    {
                      "cluster_id": "cl_a1fa8d618bc09b6c",
                      "confidence": 1,
                      "reasons": [
                        "normalised_email_match"
                      ],
                      "action": "auto",
                      "record_ids": [
                        "idx0",
                        "idx1"
                      ],
                      "member_count": 2,
                      "golden_record": {
                        "email": {
                          "value": "pat@acme.com",
                          "source_record_id": "idx0",
                          "origin": "cached",
                          "why": "tie"
                        },
                        "person_name": {
                          "value": "Patrick Lee",
                          "source_record_id": "idx1",
                          "origin": "cached",
                          "why": "completeness"
                        },
                        "company_raw": {
                          "value": "Acme Inc",
                          "source_record_id": "idx1",
                          "origin": "cached",
                          "why": "completeness"
                        }
                      },
                      "conflicts": []
                    }
                  ],
                  "summary": {
                    "records_in": 2,
                    "clusters": 1,
                    "records_reduced": 1,
                    "needs_review": 0
                  }
                }
              }
            }
          },
          "202": {
            "description": "Large managed-list dedupe accepted for asynchronous processing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobHandle"
                },
                "example": {
                  "job_id": "23",
                  "status": "queued",
                  "poll_url": "https://ratifai.app/hook/FlowClick/FlowVerify/v1/jobs?id=23"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body (neither records nor list_id).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The list_id does not exist for your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Inline records array exceeds the synchronous cap of 1000.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/hygiene": {
      "post": {
        "operationId": "manageHygiene",
        "summary": "Register, unregister, or report continuous hygiene for a list",
        "description": "Enrol a list for continuous hygiene (scheduled re-validation plus decay alerts), turn it off, or report its status. Re-validating a record already under management is free and unlimited (billing rule 5); registration itself deducts no credits. A record is 'under management' when it belongs to a list with an active hygiene registration. Gated by plan: hygiene needs a paid plan; Clean allows one hygiene list up to 25,000 records; Clean Pro and above allow unlimited lists up to their per-plan size cap. On a plan limit the response is 403 and its message names the limit and the upgrade. A list not owned by the caller returns 404.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HygieneRequest"
              },
              "example": {
                "list_id": "LST-00007",
                "action": "register",
                "cadence": "30"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Current hygiene state for the list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HygieneResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing list_id"
          },
          "403": {
            "description": "Plan limit reached; message names the limit and the upgrade"
          },
          "404": {
            "description": "No list with that id exists for the caller's account"
          }
        }
      }
    },
    "/find": {
      "post": {
        "operationId": "findEmail",
        "summary": "Find a person's email address",
        "description": "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, then a learned-pattern cache, then generic ranked patterns) and probes candidates over SMTP, stopping at the first deliverable. On a catch-all domain it returns the most likely address marked verified:false rather than a false positive. BUNDLED: a find deducts zero credits; it is gated by plan (paid plans only) and by the shared per-key rate limit. Finds a named individual only — it will not enumerate the addresses at a domain (send first_name and last_name).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FindRequest"
              },
              "example": {
                "first_name": "Dan",
                "last_name": "Ludowise",
                "domain": "example.com"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Makes the call safe to retry. Wins over an idempotency_key body field if both are present."
          }
        ],
        "responses": {
          "200": {
            "description": "A find result. verified:true is a probe-confirmed address; verified:false with catch_all:true is an honest best guess on a catch-all domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FindResponse"
                },
                "examples": {
                  "verified": {
                    "summary": "Verified from corpus",
                    "value": {
                      "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"
                    }
                  },
                  "catch_all": {
                    "summary": "Catch-all domain (honest best guess)",
                    "value": {
                      "email": "jane.doe@stripe-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": "stripe-example.com"
                    }
                  },
                  "not_found": {
                    "summary": "No address confirmed",
                    "value": {
                      "email": "",
                      "found": false,
                      "verified": false,
                      "confidence": 0,
                      "pattern": "first.last",
                      "pattern_source": "generic",
                      "pattern_evidence_count": 0,
                      "candidates_probed": 5,
                      "alternatives": [],
                      "reason": "No candidate address was accepted at example.com after probing 5 candidate(s); no deliverable address could be confirmed.",
                      "catch_all": false,
                      "corpus_hit": false,
                      "request_id": "req_01J...",
                      "domain_used": "example.com"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request — first_name and last_name are required, and a domain or company must be supplied. Requests shaped as domain enumeration (no name) are refused here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The email finder is not available on your plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (shared across REST and MCP). Retry after the window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/export": {
      "post": {
        "operationId": "exportAccount",
        "summary": "Export all account data",
        "description": "Export everything your account owns as a single JSON archive, one section per entity (records, validations, lists, jobs, dedupe clusters, golden records, score snapshots and outcomes, users, usage, billing history, webhook endpoints, hygiene registrations). It never includes API-key material or password hashes. FREE: an export deducts zero credits. A large export can be run as an async job served by the same signed job-completion webhook; pass async:true to force that path.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "async": {
                    "type": "boolean",
                    "description": "Run the export as an async job delivered via the signed job webhook instead of returning the archive inline."
                  }
                }
              },
              "example": {
                "async": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The account export archive, one section per entity. Never includes API keys or password hashes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "202": {
            "description": "Export accepted as an async job; the archive is delivered via the signed job-completion webhook.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "job_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/account": {
      "delete": {
        "operationId": "deleteAccount",
        "summary": "Schedule account deletion",
        "description": "Schedule permanent deletion of your entire account and all tenant-scoped data after a 30-day grace period. Deletion is confirmed, not one click: it requires a second explicit confirmation parameter. During the grace period the account is suspended but recoverable; your API keys stop working immediately. Billing/tax records (detached, 7 years), aggregate non-identifying usage counters, and the cross-tenant corpus and suppression safety list survive, as documented under Data retention.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "confirm",
                  "confirmation"
                ],
                "properties": {
                  "confirm": {
                    "type": "boolean",
                    "description": "Must be true."
                  },
                  "confirmation": {
                    "type": "string",
                    "enum": [
                      "DELETE"
                    ],
                    "description": "Must be the literal string DELETE."
                  }
                }
              },
              "example": {
                "confirm": true,
                "confirmation": "DELETE"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Deletion scheduled; the account is now pending deletion and recoverable until the grace period ends.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "PendingDeletion"
                    },
                    "grace_until": {
                      "type": "string"
                    },
                    "grace_days": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Confirmation required or incorrect; deletion was not scheduled."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/server-test": {
      "post": {
        "operationId": "serverTest",
        "summary": "Deliverability server test (DNS-records diagnostic)",
        "description": "Run a read-only deliverability diagnostic on a sending domain: MX, SPF, DMARC, reverse DNS and — when a DKIM selector is supplied — DKIM, plus a coarse SMTP reachability signal. Every check returns a status (pass / warn / fail / info / unknown) and plain-language remediation you can act on. No mail is sent and no mailbox is contacted beyond the coarse reachability probe. BUNDLED: this endpoint deducts zero credits. It is gated to the Clean Pro plan and above and by the shared per-key rate limit. Results are cached per (domain, selector) for about 15 minutes. Reverse DNS (PTR) is included: the domain's primary mail host is checked for a PTR record and forward-confirmed against its address.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ServerTestRequest"
              },
              "example": {
                "domain": "example.com",
                "selector": "selector1"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A deliverability report. overall is the worst check status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServerTestResponse"
                },
                "example": {
                  "request_id": "req_1786129359932",
                  "cached": false,
                  "result": {
                    "domain": "example.com",
                    "selector": "",
                    "overall": "warn",
                    "checks": [
                      {
                        "check": "mx",
                        "status": "pass",
                        "title": "MX records present",
                        "detail": "The domain has 1 MX record(s), in priority order: 10 mail.example.com.",
                        "remediation": "No action needed. Confirm the lowest-numbered host is your primary inbound mail server."
                      },
                      {
                        "check": "dmarc",
                        "status": "warn",
                        "title": "No DMARC record",
                        "detail": "There is no DMARC record at _dmarc.example.com, so receivers have no instruction for mail that fails SPF or DKIM.",
                        "remediation": "Publish a TXT record at _dmarc.example.com such as: v=DMARC1; p=none; rua=mailto:dmarc@example.com . Start at p=none, then tighten to quarantine and reject."
                      },
                      {
                        "check": "reverse_dns",
                        "status": "pass",
                        "title": "Reverse DNS present and forward-confirmed",
                        "detail": "The mail host mail.example.com (203.0.113.10) has a PTR record of mail.example.com, and mail.example.com resolves back to 203.0.113.10.",
                        "remediation": "No action needed."
                      }
                    ],
                    "reverse_dns": {
                      "status": "pass",
                      "host": "mail.example.com",
                      "ip": "203.0.113.10",
                      "ptr": "mail.example.com",
                      "forward_confirmed": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — domain is missing or malformed."
          },
          "401": {
            "description": "invalid_api_key / revoked_api_key — authentication failed."
          },
          "403": {
            "description": "forbidden — the caller's plan is below Clean Pro."
          },
          "429": {
            "description": "rate_limited — the shared per-key rate limit was exceeded."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A VerifAi API key, e.g. vk_live_..."
      }
    },
    "schemas": {
      "ValidateRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "description": "The address to verify."
          },
          "depth": {
            "type": "string",
            "enum": [
              "syntax",
              "mx_only",
              "full"
            ],
            "default": "full",
            "description": "How deep to probe. Degraded to your plan maximum."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Makes the call safe to retry. The Idempotency-Key header wins if both are given."
          }
        }
      },
      "Check": {
        "type": "object",
        "properties": {
          "value": {
            "type": "boolean"
          },
          "valid": {
            "type": "boolean"
          },
          "accepted": {
            "type": "boolean"
          },
          "status": {
            "type": "string"
          },
          "records": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "origin": {
            "type": "string",
            "description": "Provenance of this field: checked, cached, inferred, assumed, skipped or failed_open."
          }
        }
      },
      "ValidateResponse": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string"
          },
          "verdict": {
            "type": "string",
            "enum": [
              "deliverable",
              "undeliverable",
              "risky",
              "unknown"
            ]
          },
          "reason": {
            "type": "string",
            "description": "Machine-readable reason code."
          },
          "depth_requested": {
            "type": "string"
          },
          "depth_applied": {
            "type": "string"
          },
          "billable": {
            "type": "boolean"
          },
          "credits_charged": {
            "type": "integer",
            "enum": [
              0,
              1
            ]
          },
          "checks": {
            "type": "object",
            "properties": {
              "syntax": {
                "$ref": "#/components/schemas/Check"
              },
              "role_address": {
                "$ref": "#/components/schemas/Check"
              },
              "disposable": {
                "$ref": "#/components/schemas/Check"
              },
              "typo_suggestion": {
                "type": "string",
                "nullable": true
              },
              "mx": {
                "$ref": "#/components/schemas/Check"
              },
              "smtp": {
                "$ref": "#/components/schemas/Check"
              },
              "catch_all": {
                "$ref": "#/components/schemas/Check"
              }
            }
          },
          "mail_provider": {
            "type": "string"
          },
          "enrichment": {
            "type": "object",
            "properties": {
              "mail_provider": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "class": {
                    "type": "string"
                  },
                  "origin": {
                    "type": "string"
                  }
                }
              },
              "free_provider": {
                "$ref": "#/components/schemas/Check"
              }
            }
          },
          "evidence": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "at": {
                  "type": "string",
                  "format": "date-time"
                },
                "step": {
                  "type": "string"
                },
                "detail": {
                  "type": "string"
                }
              }
            }
          },
          "request_id": {
            "type": "string"
          },
          "quality_score": {
            "$ref": "#/components/schemas/QualityScore"
          }
        }
      },
      "EnrichRequest": {
        "type": "object",
        "description": "Any subset of the fields may be supplied; all are optional.",
        "properties": {
          "email": {
            "type": "string",
            "description": "An email address to normalize and classify."
          },
          "name": {
            "type": "string",
            "description": "A person's name to split and canonicalize."
          },
          "phone": {
            "type": "string",
            "description": "A phone number to normalize to E.164 and describe."
          },
          "address": {
            "type": "string",
            "description": "A postal address to standardize."
          },
          "company": {
            "type": "string",
            "description": "A company name to normalize."
          },
          "website": {
            "type": "string",
            "description": "A website URL to normalize."
          }
        }
      },
      "EnrichResponse": {
        "type": "object",
        "description": "Enriched record. Each value field has a sibling <field>_origin of checked or skipped.",
        "properties": {
          "normalized_email": {
            "type": "string"
          },
          "normalized_email_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "email_domain": {
            "type": "string"
          },
          "email_domain_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "is_corporate": {
            "type": "string"
          },
          "is_corporate_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "name_first": {
            "type": "string"
          },
          "name_first_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "name_last": {
            "type": "string"
          },
          "name_last_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "name_canonical": {
            "type": "string"
          },
          "name_canonical_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "phone_e164": {
            "type": "string"
          },
          "phone_e164_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "phone_national": {
            "type": "string"
          },
          "phone_national_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "phone_area_code": {
            "type": "string"
          },
          "phone_area_code_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "phone_timezone": {
            "type": "string"
          },
          "phone_timezone_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "company_normalized": {
            "type": "string"
          },
          "company_normalized_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "company_legal_form": {
            "type": "string"
          },
          "company_legal_form_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "address_standardized": {
            "type": "string"
          },
          "address_standardized_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "website_normalized": {
            "type": "string"
          },
          "website_normalized_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "completeness_score": {
            "type": "string"
          },
          "completeness_score_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "dedupe_key": {
            "type": "string"
          },
          "dedupe_key_origin": {
            "type": "string",
            "enum": [
              "checked",
              "skipped"
            ]
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "BatchRequest": {
        "type": "object",
        "required": [
          "emails"
        ],
        "properties": {
          "emails": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 10000,
            "description": "The addresses to verify. Up to 10,000 per batch; an empty array is rejected."
          },
          "depth": {
            "type": "string",
            "enum": [
              "syntax",
              "mx_only",
              "full"
            ],
            "default": "full",
            "description": "How deep to probe. Degraded to your plan maximum."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Makes the submission safe to retry. The Idempotency-Key header wins if both are given."
          }
        }
      },
      "JobHandle": {
        "type": "object",
        "description": "Returned by POST /files and POST /validate/batch.",
        "properties": {
          "job_id": {
            "type": "string",
            "description": "VerifAi's opaque job id."
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "completed",
              "failed"
            ]
          },
          "poll_url": {
            "type": "string",
            "format": "uri",
            "description": "Exact URL to poll for status. Read it; do not build it."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "JobStatus": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "completed",
              "failed"
            ]
          },
          "progress": {
            "type": "object",
            "properties": {
              "processed": {
                "type": "integer"
              },
              "total": {
                "type": "integer"
              },
              "percent": {
                "type": "integer"
              }
            }
          },
          "counts_by_verdict": {
            "type": "object",
            "properties": {
              "deliverable": {
                "type": "integer"
              },
              "undeliverable": {
                "type": "integer"
              },
              "risky": {
                "type": "integer"
              },
              "unknown": {
                "type": "integer"
              }
            }
          },
          "credits_charged": {
            "type": "integer",
            "description": "Credits spent so far. Expected to be lower than progress.processed since only billable rows are charged."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "result_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Exact URL to download the result CSV; null until status is completed. Read it; do not build it. Lives at the sibling path /v1result/job-result."
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "CreditsResponse": {
        "type": "object",
        "properties": {
          "plan": {
            "type": "string"
          },
          "credits_balance": {
            "type": "integer"
          },
          "payg_credits": {
            "type": "integer"
          },
          "balance": {
            "type": "integer"
          },
          "included_allowance": {
            "type": "integer"
          },
          "period": {
            "type": "string"
          },
          "period_reset": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "invalid_api_key",
                  "revoked_api_key",
                  "insufficient_credits",
                  "forbidden",
                  "not_found",
                  "job_not_found",
                  "job_not_ready",
                  "no_email_column",
                  "file_empty",
                  "list_too_large",
                  "batch_too_large",
                  "rate_limited",
                  "internal_error",
                  "list_not_found",
                  "payload_too_large"
                ]
              },
              "message": {
                "type": "string"
              },
              "doc_url": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "ListHealthResponse": {
        "type": "object",
        "properties": {
          "grade": {
            "type": "string",
            "enum": [
              "A",
              "B",
              "C",
              "D",
              "F"
            ],
            "description": "From published bands: A>=90, B>=75, C>=60, D>=40, else F (on deliverable_pct)."
          },
          "total": {
            "type": "integer"
          },
          "distribution": {
            "type": "object",
            "properties": {
              "deliverable": {
                "type": "integer"
              },
              "undeliverable": {
                "type": "integer"
              },
              "risky": {
                "type": "integer"
              },
              "unknown": {
                "type": "integer"
              }
            }
          },
          "deliverable_pct": {
            "type": "number"
          },
          "decay": {
            "type": "object",
            "description": "On a list checked only once, this is { status: \"not_enough_history\" } instead.",
            "properties": {
              "since_last_check_days": {
                "type": "integer"
              },
              "newly_undeliverable": {
                "type": "integer"
              },
              "decay_pct": {
                "type": "number"
              },
              "status": {
                "type": "string"
              }
            }
          },
          "enrichment_lift": {
            "type": "object",
            "properties": {
              "before": {
                "type": "object",
                "properties": {
                  "phone": {
                    "type": "integer"
                  },
                  "company": {
                    "type": "integer"
                  },
                  "address": {
                    "type": "integer"
                  },
                  "name_parsed": {
                    "type": "integer"
                  }
                }
              },
              "after": {
                "type": "object",
                "properties": {
                  "phone": {
                    "type": "integer"
                  },
                  "company": {
                    "type": "integer"
                  },
                  "address": {
                    "type": "integer"
                  },
                  "name_parsed": {
                    "type": "integer"
                  }
                }
              },
              "fields_added": {
                "type": "integer"
              },
              "records_improved": {
                "type": "integer"
              },
              "lift_pct": {
                "type": "number"
              }
            }
          },
          "risks": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "disposable_domains",
                "role_addresses",
                "typo_domains"
              ]
            }
          },
          "computed_at": {
            "type": "string",
            "format": "date-time"
          },
          "billable": {
            "type": "boolean"
          },
          "cost": {
            "type": "string"
          },
          "request_id": {
            "type": "string"
          },
          "score_distribution": {
            "type": "object",
            "description": "Count of records in each quality band, alongside the verdict distribution.",
            "properties": {
              "A": {
                "type": "integer"
              },
              "B": {
                "type": "integer"
              },
              "C": {
                "type": "integer"
              },
              "D": {
                "type": "integer"
              },
              "F": {
                "type": "integer"
              },
              "scored": {
                "type": "integer"
              },
              "unscored": {
                "type": "integer"
              }
            }
          }
        }
      },
      "EvaluateRequest": {
        "type": "object",
        "description": "Provide emails/addresses as a JSON array, or send a raw CSV / newline list as text/csv.",
        "properties": {
          "emails": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The addresses to evaluate."
          },
          "addresses": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Alias for emails."
          }
        }
      },
      "EvaluateResponse": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer"
          },
          "unique": {
            "type": "integer"
          },
          "exact_duplicates": {
            "type": "integer"
          },
          "syntactically_invalid": {
            "type": "integer"
          },
          "disposable": {
            "type": "integer"
          },
          "role_addresses": {
            "type": "integer"
          },
          "typo_domains": {
            "type": "integer"
          },
          "suggested_corrections": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "domain": {
                  "type": "string"
                },
                "suggestion": {
                  "type": "string"
                }
              }
            }
          },
          "domain_distribution": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "domain": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                }
              }
            }
          },
          "top_domain_concentration_pct": {
            "type": "integer"
          },
          "grade": {
            "type": "string",
            "enum": [
              "A",
              "B",
              "C",
              "D",
              "F"
            ]
          },
          "quality_pct": {
            "type": "number"
          },
          "limitations": {
            "type": "string"
          },
          "upgrade": {
            "type": "string"
          },
          "depth": {
            "type": "string",
            "enum": [
              "syntax"
            ]
          },
          "billable": {
            "type": "boolean"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "DedupeRequest": {
        "type": "object",
        "description": "Provide exactly one of records or list_id.",
        "properties": {
          "records": {
            "type": "array",
            "maxItems": 1000,
            "items": {
              "type": "object",
              "properties": {
                "email": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "phone": {
                  "type": "string"
                },
                "company": {
                  "type": "string"
                },
                "website": {
                  "type": "string"
                },
                "address": {
                  "type": "string"
                }
              }
            },
            "description": "Inline records to deduplicate against each other (max 1000 for synchronous processing)."
          },
          "list_id": {
            "type": "string",
            "description": "Id of a managed list to deduplicate."
          }
        }
      },
      "DedupeGoldenField": {
        "type": "object",
        "properties": {
          "value": {
            "type": "string"
          },
          "source_record_id": {
            "type": "string",
            "description": "The contributing record this value survived from."
          },
          "origin": {
            "type": "string",
            "enum": [
              "checked",
              "cached",
              "skipped"
            ]
          },
          "why": {
            "type": "string",
            "description": "The survivorship rule that selected this value."
          }
        }
      },
      "DedupeCluster": {
        "type": "object",
        "properties": {
          "cluster_id": {
            "type": "string"
          },
          "confidence": {
            "type": "number",
            "format": "float",
            "description": "0..1; how conclusive the match is."
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Signals that fired, e.g. normalised_email_match, phone_match."
          },
          "action": {
            "type": "string",
            "enum": [
              "auto",
              "review"
            ],
            "description": "auto merges only on effectively conclusive evidence; everything else is review."
          },
          "member_count": {
            "type": "integer"
          },
          "record_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "golden_record": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/DedupeGoldenField"
            }
          },
          "conflicts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "field": {
                  "type": "string"
                },
                "candidates": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "record_id": {
                        "type": "string"
                      },
                      "value": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "DedupeResponse": {
        "type": "object",
        "properties": {
          "clusters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DedupeCluster"
            }
          },
          "summary": {
            "type": "object",
            "properties": {
              "records_in": {
                "type": "integer"
              },
              "clusters": {
                "type": "integer"
              },
              "records_reduced": {
                "type": "integer"
              },
              "needs_review": {
                "type": "integer"
              }
            }
          }
        }
      },
      "HygieneRequest": {
        "type": "object",
        "required": [
          "list_id"
        ],
        "properties": {
          "list_id": {
            "type": "string",
            "description": "The list id (e.g. LST-00007)."
          },
          "action": {
            "type": "string",
            "enum": [
              "register",
              "unregister",
              "status"
            ],
            "default": "status",
            "description": "register enrols the list; unregister turns it off; status reports current state."
          },
          "cadence": {
            "type": "string",
            "enum": [
              "30",
              "60",
              "90"
            ],
            "description": "register only: days between automatic re-validations (default 30)."
          }
        }
      },
      "HygieneResponse": {
        "type": "object",
        "properties": {
          "list_id": {
            "type": "string"
          },
          "hygiene": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ]
          },
          "hygiene_mode": {
            "type": "string"
          },
          "cadence_days": {
            "type": "integer",
            "nullable": true
          },
          "last_run": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "next_run": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "record_count": {
            "type": "integer"
          },
          "plan": {
            "type": "string"
          },
          "under_management_free": {
            "type": "boolean",
            "description": "Always true: re-checks of records already under management are free."
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "FindRequest": {
        "type": "object",
        "required": [
          "first_name",
          "last_name"
        ],
        "properties": {
          "first_name": {
            "type": "string",
            "description": "The person's first name.",
            "example": "Dan"
          },
          "last_name": {
            "type": "string",
            "description": "The person's last name.",
            "example": "Ludowise"
          },
          "domain": {
            "type": "string",
            "description": "The company email domain. Provide this or company.",
            "example": "example.com"
          },
          "company": {
            "type": "string",
            "description": "Company name; used only when domain is omitted. VerifAi resolves it to a domain and reports domain_used.",
            "example": "Example Inc"
          }
        }
      },
      "FindResponse": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "The most likely address, or empty when found is false.",
            "example": "dan.ludowise@example.com"
          },
          "found": {
            "type": "boolean",
            "description": "Whether an address is being returned."
          },
          "verified": {
            "type": "boolean",
            "description": "True only when a mail server confirmed the address is deliverable. Always false on catch-all domains."
          },
          "confidence": {
            "type": "number",
            "format": "float",
            "description": "0-1 confidence in the returned address.",
            "example": 0.93
          },
          "pattern": {
            "type": "string",
            "description": "The address pattern, e.g. first.last.",
            "example": "first.last"
          },
          "pattern_source": {
            "type": "string",
            "enum": [
              "corpus",
              "learned_cache",
              "generic"
            ],
            "description": "How the pattern was derived: corpus = inferred from addresses VerifAi has actually verified; learned_cache = the domain's previously learned pattern; generic = ranked common patterns."
          },
          "pattern_evidence_count": {
            "type": "integer",
            "description": "For corpus/learned_cache, how many observations back the pattern.",
            "example": 41
          },
          "candidates_probed": {
            "type": "integer",
            "description": "Total mailbox probes performed for this find, including catch-all detection. Capped by plan (default 5).",
            "example": 1
          },
          "alternatives": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Other plausible addresses (never verified, never suppressed)."
          },
          "reason": {
            "type": "string",
            "description": "Plain-language explanation of the result."
          },
          "catch_all": {
            "type": "boolean",
            "description": "True when the domain accepts all mail; no address can be confirmed by probing."
          },
          "corpus_hit": {
            "type": "boolean",
            "description": "True when the returned pattern came from the corpus."
          },
          "request_id": {
            "type": "string",
            "example": "req_01J..."
          },
          "domain_used": {
            "type": "string",
            "description": "The domain the find ran against (echoes domain, or the domain resolved from company).",
            "example": "example.com"
          }
        }
      },
      "QualityFactor": {
        "type": "object",
        "description": "One factor contributing to the quality score.",
        "properties": {
          "factor": {
            "type": "string",
            "example": "role_address"
          },
          "direction": {
            "type": "string",
            "enum": [
              "+",
              "-"
            ],
            "description": "Whether the factor helped (+) or hurt (-) quality."
          },
          "weight": {
            "type": "integer",
            "description": "Signed point contribution to the score.",
            "example": -12
          },
          "detail": {
            "type": "string",
            "example": "Addressed to a role such as info@ or sales@, not a named person."
          }
        }
      },
      "QualityScore": {
        "type": "object",
        "description": "Deterministic, explainable quality score for the address. Distinct from the verdict: it answers whether the address is worth sending to, never whether it is deliverable. Free on every surface. When evidence is insufficient (e.g. an unknown verdict), available is false and score/band are null - no number is invented.",
        "properties": {
          "available": {
            "type": "boolean",
            "description": "False when the score cannot be computed; score and band are then null."
          },
          "score": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 100,
            "example": 38
          },
          "band": {
            "type": "string",
            "nullable": true,
            "enum": [
              "A",
              "B",
              "C",
              "D",
              "F"
            ],
            "description": "A-F band, same cut-offs as list health (90/75/60/40).",
            "example": "F"
          },
          "reason": {
            "type": "string",
            "description": "Present only when available is false (e.g. unknown_verdict, not_validated)."
          },
          "factors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QualityFactor"
            }
          },
          "explanation": {
            "type": "string",
            "example": "Scored 38 out of 100 (band F) - a low-quality address to send to."
          }
        }
      },
      "ServerTestRequest": {
        "type": "object",
        "required": [
          "domain"
        ],
        "properties": {
          "domain": {
            "type": "string",
            "description": "The sending domain to test (the part after the @). Scheme and @ are stripped.",
            "example": "example.com"
          },
          "selector": {
            "type": "string",
            "description": "Optional DKIM selector. Supply it to also check the DKIM key at <selector>._domainkey.<domain>. Find it as the s= value in a DKIM-Signature header, or from your mail provider.",
            "example": "selector1"
          }
        }
      },
      "ServerTestCheck": {
        "type": "object",
        "properties": {
          "check": {
            "type": "string",
            "enum": [
              "mx",
              "spf",
              "dmarc",
              "dkim",
              "reachability",
              "reverse_dns"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pass",
              "warn",
              "fail",
              "info",
              "unknown"
            ],
            "description": "fail and warn are actionable; unknown means the lookup was inconclusive (e.g. throttled) — re-run."
          },
          "title": {
            "type": "string"
          },
          "detail": {
            "type": "string",
            "description": "What was found, in plain language."
          },
          "remediation": {
            "type": "string",
            "description": "What to do about it — actionable for a non-specialist."
          }
        }
      },
      "ServerTestResult": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string"
          },
          "selector": {
            "type": "string"
          },
          "overall": {
            "type": "string",
            "enum": [
              "pass",
              "warn",
              "fail",
              "unknown"
            ],
            "description": "The worst status across the checks."
          },
          "checks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ServerTestCheck"
            }
          },
          "reverse_dns": {
            "type": "object",
            "description": "Structured summary of the reverse-DNS (PTR) check on the domain's primary mail host. The same finding also appears in checks[] with a human-readable title and remediation.",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "pass",
                  "warn",
                  "info",
                  "unknown"
                ],
                "description": "pass = PTR present and forward-confirmed; warn = no PTR, or PTR does not forward-confirm; unknown = could not check (throttled/temporarily unavailable); info = no MX host to check."
              },
              "host": {
                "type": "string",
                "description": "The primary (lowest-preference) mail host checked."
              },
              "ip": {
                "type": "string",
                "description": "The IP address resolved for the mail host (IPv4 preferred, else IPv6)."
              },
              "ptr": {
                "type": "string",
                "description": "The PTR (reverse-DNS) name for the IP, or empty if none."
              },
              "forward_confirmed": {
                "type": "boolean",
                "nullable": true,
                "description": "true if the PTR name resolves back to the same IP; false if it does not (or no PTR); null if not determined."
              }
            }
          }
        }
      },
      "ServerTestResponse": {
        "type": "object",
        "properties": {
          "request_id": {
            "type": "string"
          },
          "cached": {
            "type": "boolean",
            "description": "true when served from the short-lived per-(domain,selector) cache."
          },
          "result": {
            "$ref": "#/components/schemas/ServerTestResult"
          }
        }
      }
    },
    "responses": {
      "InvalidRequest": {
        "description": "Malformed request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_request",
                "message": "The request was malformed - check the required fields and value types.",
                "doc_url": "https://ratifai.app/docs-errors.html#invalid_request"
              },
              "request_id": "req_..."
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, invalid or revoked API key. Code is invalid_api_key or revoked_api_key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_api_key",
                "message": "The API key is missing, malformed, revoked, or expired.",
                "doc_url": "https://ratifai.app/docs-errors.html#invalid_api_key"
              },
              "request_id": "req_..."
            }
          }
        }
      },
      "InsufficientCredits": {
        "description": "No credits left for a billable check. For jobs, only refused up front when balance is already zero and the requested depth is billable.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "insufficient_credits",
                "message": "Your account has no credits remaining for a billable check.",
                "doc_url": "https://ratifai.app/docs-errors.html#insufficient_credits"
              },
              "request_id": "req_..."
            }
          }
        }
      },
      "Forbidden": {
        "description": "The key is valid but not permitted this action.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "forbidden",
                "message": "Your plan does not permit this action.",
                "doc_url": "https://ratifai.app/docs-errors.html#forbidden"
              },
              "request_id": "req_..."
            }
          }
        }
      },
      "JobNotFound": {
        "description": "No job with that id exists for your account. Returned instead of 403 so the id is not confirmed to exist elsewhere.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "job_not_found",
                "message": "No job with that id exists for your account.",
                "doc_url": "https://ratifai.app/docs-errors.html#job_not_found"
              },
              "request_id": "req_..."
            }
          }
        }
      },
      "JobNotReady": {
        "description": "The job result is not ready yet. Poll the job status until it reports completed, then download the result.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "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_..."
            }
          }
        }
      },
      "NoEmailColumn": {
        "description": "No email column could be detected in the uploaded file.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "no_email_column",
                "message": "No email column could be detected in the uploaded file. Supply a column mapping or include a column named 'email'.",
                "doc_url": "https://ratifai.app/docs-errors.html#no_email_column"
              },
              "request_id": "req_..."
            }
          }
        }
      },
      "FileEmpty": {
        "description": "The uploaded file is empty or contains no data rows.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "file_empty",
                "message": "The uploaded file is empty or contains no data rows.",
                "doc_url": "https://ratifai.app/docs-errors.html#file_empty"
              },
              "request_id": "req_..."
            }
          }
        }
      },
      "ListTooLarge": {
        "description": "The uploaded file exceeds your plan's list-size limit.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "list_too_large",
                "message": "This file exceeds your plan's list-size limit.",
                "doc_url": "https://ratifai.app/docs-errors.html#list_too_large"
              },
              "request_id": "req_..."
            }
          }
        }
      },
      "BatchTooLarge": {
        "description": "The emails array exceeds the maximum of 10000. Upload larger lists via POST /files.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "batch_too_large",
                "message": "The emails array exceeds the maximum of 10000. Upload larger lists via POST /v1/files.",
                "doc_url": "https://ratifai.app/docs-errors.html#batch_too_large"
              },
              "request_id": "req_..."
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-key, per-minute rate limit exceeded.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait before retrying."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "You are sending requests faster than your plan allows.",
                "doc_url": "https://ratifai.app/docs-errors.html#rate_limited"
              },
              "request_id": "req_..."
            }
          }
        }
      },
      "InternalError": {
        "description": "Server error; safe to retry with backoff. A billable check that errors is not charged.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "internal_error",
                "message": "Something failed on our side. It is safe to retry with backoff.",
                "doc_url": "https://ratifai.app/docs-errors.html#internal_error"
              },
              "request_id": "req_..."
            }
          }
        }
      },
      "ListNotFound": {
        "description": "No list with that id exists for your account. Returned instead of 403 so the id is not confirmed to exist elsewhere.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "list_not_found",
                "message": "No list with that id exists for your account.",
                "doc_url": "https://ratifai.app/docs-errors.html#list_not_found"
              },
              "request_id": "req_..."
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The request body has too many addresses for POST /evaluate. Anonymously the cap is 5,000; authenticate or use POST /files.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "payload_too_large",
                "message": "The request body is too large for this endpoint. Reduce the number of addresses or upload via POST /v1/files.",
                "doc_url": "https://ratifai.app/docs-errors.html#payload_too_large"
              },
              "request_id": "req_..."
            }
          }
        }
      }
    }
  }
}