{"openapi":"3.1.0","info":{"title":"Vouch API","version":"1.0.0","description":"Email verification with the receipts: syntax, live MX lookup, a real SMTP RCPT TO handshake and catch-all detection, folded into one deliverability verdict. Vouch never sends an email to verify one.","contact":{"name":"Datapad","email":"hello@datapad.nl","url":"https://www.datapad.nl"}},"servers":[{"url":"https://vouch.fast"}],"security":[{"bearerAuth":[]},{"apiKeyQuery":[]}],"paths":{"/v1/verify":{"get":{"operationId":"verifyEmail","summary":"Verify one email address","description":"Runs the full pipeline — syntax, MX, SMTP probe, catch-all — short-circuiting the network when an earlier stage settles the verdict. About one second typical; eight seconds worst case.","parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string"},"example":"ada@lovelace.dev"}],"responses":{"200":{"description":"The verification verdict.","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Per-key requests allowed per minute."},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-Credits-Remaining":{"schema":{"type":"integer"},"description":"Present when credit enforcement is active."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResult"}}}},"400":{"description":"Missing ?email= (error: missing_email).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown or deactivated API key (error: missing_api_key | invalid_api_key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Credit balance empty (error: insufficient_credits). Header X-Credits-Remaining: 0.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded (error: rate_limited). Respect the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Vouch-side failure (error: server_error) — never charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/batches":{"post":{"operationId":"createBatch","summary":"Upload a list to verify (CSV or Excel)","description":"Scans every cell for addresses, lowercases and de-duplicates, then verifies each unique address in parallel as a background job. Max 20 MB and 10,000 unique addresses per job.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"A .csv or .xlsx file."}}}}}},"responses":{"202":{"description":"Job accepted; verification continues in the background.","content":{"application/json":{"schema":{"type":"object","required":["jobId","status","total","report"],"properties":{"jobId":{"type":"string"},"status":{"type":"string","const":"pending"},"total":{"type":"integer","description":"Unique addresses queued (what the job can charge at most)."},"report":{"$ref":"#/components/schemas/DedupReport"}}}}}},"400":{"description":"Not multipart, no 'file' field, or no addresses found in the file.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Credit balance empty (error: insufficient_credits).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"File exceeds the 20 MB upload limit (error: file_too_large).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Could not start the batch.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listBatches","summary":"List your recent batch jobs","description":"Newest first. Not counted against the per-key rate limit.","responses":{"200":{"description":"Your batch jobs.","content":{"application/json":{"schema":{"type":"object","required":["batches"],"properties":{"batches":{"type":"array","items":{"$ref":"#/components/schemas/BatchJob"}}}}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/batches/{id}":{"get":{"operationId":"getBatch","summary":"Batch job status & progress","description":"Poll until status is 'complete', then download /v1/batches/{id}/results. Not counted against the per-key rate limit.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The job.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchJob"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such batch for this account (error: not_found).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/batches/{id}/results":{"get":{"operationId":"getBatchResults","summary":"Download the results CSV","description":"One row per verified address. Columns: email, status, reason, score, disposable, roleAccount, freeProvider, catchAll, didYouMean, mxHost.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The results CSV.","content":{"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such batch for this account (error: not_found).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Job not complete yet (error: not_ready).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/credits":{"get":{"operationId":"getCredits","summary":"Your current credit balance","description":"Credit-free pre-flight — read the calling key's remaining credits before starting a batch, so you can warn when a list is larger than the balance. Not counted against the per-key rate limit. When credit enforcement is off the account isn't metered, so enforced is false and creditsRemaining is null (treat as unlimited).","responses":{"200":{"description":"The key's credit balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditBalance"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/health-check":{"post":{"operationId":"createHealthCheck","summary":"Start your free List Health Check","description":"Structural-only diagnosis of a whole list — syntax, disposable domains, MX records, role accounts. No SMTP probe, no credit charge. One free check per API key: the first POST starts a background job (202); a later POST replays that job (200 with alreadyUsed: true) unless it failed. The report is aggregate-only — counts and percentages, never per-address verdicts.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["emails"],"properties":{"emails":{"type":"array","items":{"type":"string"},"maxItems":50000,"description":"Up to 50,000 addresses; de-duplicated on intake."}}}}}},"responses":{"200":{"description":"This key's free check already ran — the existing job is returned (alreadyUsed: true); the new payload is ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthCheckJob"}}}},"202":{"description":"Job started — poll GET /v1/health-check.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthCheckJob"}}}},"400":{"description":"Bad request (error: invalid_body | missing_emails | too_many_emails | no_emails_found).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Could not start the health check (error: server_error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"getHealthCheck","summary":"Poll your health-check job","description":"The calling key's health-check job; the aggregate report is present once status is complete. Not counted against the per-key rate limit.","responses":{"200":{"description":"The job (report present once complete).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthCheckJob"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No free health check has been run for this key yet (error: not_found).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key from the dashboard, format vch_live_…"},"apiKeyQuery":{"type":"apiKey","in":"query","name":"apikey","description":"Fallback when headers are awkward; prefer the Authorization header."}},"schemas":{"Error":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"Machine-readable code, e.g. invalid_api_key, rate_limited."},"message":{"type":"string"},"retryAfter":{"type":"integer","description":"Seconds to wait — only on 429 responses."}}},"VerifyResult":{"type":"object","required":["email","user","domain","status","reason","score","checks","flags","didYouMean","mxHost","meta"],"properties":{"email":{"type":"string"},"user":{"type":"string"},"domain":{"type":"string"},"status":{"type":"string","enum":["deliverable","risky","undeliverable","unknown"]},"reason":{"type":"string","enum":["invalid_syntax","no_mx_records","mailbox_not_found","mailbox_exists","catch_all","role_account","disposable_domain","greylisted","connection_failed","timeout","unexpected_error"]},"score":{"type":"integer","minimum":0,"maximum":100},"checks":{"type":"object","required":["syntax","domainHasMx","smtpConnected","mailboxExists","catchAll"],"properties":{"syntax":{"type":"boolean"},"domainHasMx":{"type":"boolean"},"smtpConnected":{"type":"boolean"},"mailboxExists":{"type":"boolean"},"catchAll":{"type":"boolean"}}},"flags":{"type":"object","required":["disposable","roleAccount","freeProvider"],"properties":{"disposable":{"type":"boolean"},"roleAccount":{"type":"boolean"},"freeProvider":{"type":"boolean"}}},"didYouMean":{"type":["string","null"],"description":"A corrected address when the domain looks like a typo."},"mxHost":{"type":["string","null"]},"meta":{"type":"object","required":["durationMs","checkedAt"],"properties":{"durationMs":{"type":"integer"},"checkedAt":{"type":"string","format":"date-time"}}}}},"DedupReport":{"type":"object","required":["candidatesFound","duplicatesRemoved","uniqueCount","capped"],"properties":{"candidatesFound":{"type":"integer"},"duplicatesRemoved":{"type":"integer"},"uniqueCount":{"type":"integer"},"capped":{"type":"boolean","description":"True when the list exceeded the per-job cap and was truncated."}}},"CreditBalance":{"type":"object","required":["enforced","creditsRemaining"],"properties":{"enforced":{"type":"boolean","description":"False when this account isn't metering credits (treat as unlimited)."},"creditsRemaining":{"type":["integer","null"],"description":"Remaining credits; null when enforced is false."}}},"BatchJob":{"type":"object","required":["id","filename","status","total","processed","report","counts","hasResults","error","createdAt","finishedAt"],"properties":{"id":{"type":"string"},"filename":{"type":"string"},"status":{"type":"string","enum":["pending","processing","complete","failed"]},"total":{"type":"integer"},"processed":{"type":"integer"},"report":{"$ref":"#/components/schemas/DedupReport"},"counts":{"type":"object","required":["deliverable","undeliverable","risky","unknown","skipped"],"properties":{"deliverable":{"type":"integer"},"undeliverable":{"type":"integer"},"risky":{"type":"integer"},"unknown":{"type":"integer"},"skipped":{"type":"integer","description":"Addresses not verified because the credit balance ran out."}}},"hasResults":{"type":"boolean"},"error":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"}}},"CategoryStat":{"type":"object","required":["count","pct"],"properties":{"count":{"type":"integer"},"pct":{"type":"number","description":"Percent of the unique addresses checked, rounded to 1 decimal."}}},"ListHealthReport":{"type":"object","description":"Aggregate-only list diagnosis — counts and percentages per category, never per-address verdicts.","required":["total","checkedAt","breakdown","flags","summary","intake"],"properties":{"total":{"type":"integer","description":"Unique addresses checked, after de-dup and the cap."},"checkedAt":{"type":"string","format":"date-time"},"breakdown":{"type":"object","description":"Mutually exclusive categories (first match wins).","required":["invalidSyntax","disposable","noMxRecords","roleAccount","okUnverified"],"properties":{"invalidSyntax":{"$ref":"#/components/schemas/CategoryStat"},"disposable":{"$ref":"#/components/schemas/CategoryStat"},"noMxRecords":{"$ref":"#/components/schemas/CategoryStat"},"roleAccount":{"$ref":"#/components/schemas/CategoryStat"},"okUnverified":{"$ref":"#/components/schemas/CategoryStat"}}},"flags":{"type":"object","description":"Independent quality signals — one address can carry several.","required":["freeProvider","typo"],"properties":{"freeProvider":{"$ref":"#/components/schemas/CategoryStat"},"typo":{"$ref":"#/components/schemas/CategoryStat"}}},"summary":{"type":"object","description":"Roll-ups: undeliverable = invalid syntax + no MX + disposable; problematic = role accounts; unhealthy = both (the headline); deliverableUnverified = structurally clean, needs the paid SMTP check to confirm.","required":["undeliverable","problematic","unhealthy","deliverableUnverified"],"properties":{"undeliverable":{"$ref":"#/components/schemas/CategoryStat"},"problematic":{"$ref":"#/components/schemas/CategoryStat"},"unhealthy":{"$ref":"#/components/schemas/CategoryStat"},"deliverableUnverified":{"$ref":"#/components/schemas/CategoryStat"}}},"intake":{"$ref":"#/components/schemas/DedupReport"}}},"HealthCheckJob":{"type":"object","required":["jobId","status","total","intake","message"],"properties":{"jobId":{"type":"string"},"status":{"type":"string","enum":["pending","processing","complete","failed"]},"total":{"type":"integer"},"processed":{"type":"integer"},"intake":{"$ref":"#/components/schemas/DedupReport"},"report":{"anyOf":[{"$ref":"#/components/schemas/ListHealthReport"},{"type":"null"}],"description":"Present only once status is complete."},"error":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"alreadyUsed":{"type":"boolean","description":"POST only: true when this key's free check already existed and the job was replayed."},"message":{"type":"string","description":"Human-readable status / upsell line."}}}}}}