{"openapi":"3.1.0","info":{"title":"Validrow","description":"Layered email verification: syntax, normalize, typo, MX, classify, SMTP.","version":"0.1.0"},"paths":{"/v1/files":{"post":{"tags":["files"],"summary":"Upload File","description":"Store an uploaded CSV and return detected columns for mapping.","operationId":"upload_file_v1_files_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_file_v1_files_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/files/{key}/raw":{"get":{"tags":["files"],"summary":"Download Raw","description":"Serve a stored object (local-dev stand-in for a presigned URL).","operationId":"download_raw_v1_files__key__raw_get","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/jobs":{"get":{"tags":["jobs"],"summary":"List Jobs","description":"Every run this workspace has done, newest first.","operationId":"list_jobs_v1_jobs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/JobResponse"},"type":"array","title":"Response List Jobs V1 Jobs Get"}}}}}},"post":{"tags":["jobs"],"summary":"Create Job","description":"Start a run against an uploaded file.\n\nAnswers immediately with a queued job rather than holding the connection\nopen for the length of the run: a million rows is not a request. Poll the\njob, or give it a webhook and be told once.\n\nReturns 404 if the file_id is not in this workspace, and 422 if list_type is\nnot one this engine knows.","operationId":"create_job_v1_jobs_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJobRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/jobs/{job_id}":{"get":{"tags":["jobs"],"summary":"Get Job","description":"One run, with its live progress.\n\nCheap enough to poll: `processed`, `total` and `phase` come from the job\nrecord rather than from the rows, so a progress bar drawn from them is\ntruthful without costing a scan.","operationId":"get_job_v1_jobs__job_id__get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["jobs"],"summary":"Delete Job","description":"Delete a run and its cached outputs.\n\nThe validated addresses stay in the workspace by default — they are facts\nabout mailboxes, not artefacts of the run that discovered them. Pass\n``keep_addresses=false`` to drop those too.","operationId":"delete_job_v1_jobs__job_id__delete","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"keep_addresses","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Keep Addresses"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/jobs/{job_id}/download":{"get":{"tags":["jobs"],"summary":"Download Job","description":"Stream one of the three segments a finished run produces, as CSV.\n\n`cleaned` is your file back: every row, every column, with the verdict\nappended. `valid` is the rows worth sending to. `removed` is what came out,\nwith the layer and the reason each row came out.\n\nReturns 409 while the run is still working, because a segment that is not\nwritten yet is not an empty one.","operationId":"download_job_v1_jobs__job_id__download_get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"segment","in":"query","required":false,"schema":{"type":"string","pattern":"^(cleaned|valid|removed)$","default":"cleaned","title":"Segment"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/addresses":{"get":{"tags":["workspace"],"summary":"List Addresses","description":"Addresses across every run in this workspace, de-duplicated.\n\nOne row per mailbox rather than one per appearance, so an address that\narrived in three lists is counted once and carries the most recent verdict\nit was given.","operationId":"list_addresses_v1_addresses_get","parameters":[{"name":"verdict","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"title":"Verdict"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"list_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Type"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"recent","title":"Sort"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressPage"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/analytics":{"get":{"tags":["workspace"],"summary":"Analytics","description":"Everything the Analytics screen reports, in one round-trip.","operationId":"analytics_v1_analytics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Analytics V1 Analytics Get"}}}}}}},"/v1/exports":{"post":{"tags":["workspace"],"summary":"Export Slice","description":"Stream a filtered slice of the workspace as CSV.","operationId":"export_slice_v1_exports_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/health":{"get":{"tags":["meta"],"summary":"Health","description":"Whether this engine is serving, and which layers it has switched on.\n\nReachable without credentials, because a load balancer cannot log in. The\ntwo flags matter to a caller: with DNS off nothing past layer 3 can run, and\nwith the probe off a deliverable-looking address still comes back Unknown.","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/v1/verify":{"post":{"tags":["verify"],"summary":"Verify","description":"Validate a single address in real time.\n\nRuns the (blocking, DNS-touching) engine in a threadpool so the event loop\nstays free.","operationId":"verify_v1_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AddressOut":{"properties":{"email":{"type":"string","title":"Email","description":"The address as it was validated."},"domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Domain","description":"The domain part."},"status":{"type":"string","title":"Status","description":"The engine's status for this address."},"sub_status":{"type":"string","title":"Sub Status","description":"The reason under the status."},"verdict":{"type":"string","title":"Verdict","description":"The status rolled up to one of the four primary verdicts."},"score":{"type":"integer","title":"Score","description":"Confidence in the verdict, 0 to 100."},"job_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Job Id","description":"The run that produced this row."},"job_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Job Filename","description":"That run's file name."},"list_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Type","description":"The list the run belonged to."},"checked_at":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Checked At","description":"Unix time of the check."},"settled_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Settled At","description":"The layer, 1 to 7, that produced the verdict."},"mx_found":{"type":"boolean","title":"Mx Found","description":"The domain published usable MX records.","default":false},"is_catch_all":{"type":"boolean","title":"Is Catch All","description":"The domain accepts every recipient.","default":false},"is_disposable":{"type":"boolean","title":"Is Disposable","description":"The domain is a disposable provider.","default":false},"is_role":{"type":"boolean","title":"Is Role","description":"The local part is a role account.","default":false},"is_free":{"type":"boolean","title":"Is Free","description":"The domain is a free consumer provider.","default":false}},"type":"object","required":["email","status","sub_status","verdict","score"],"title":"AddressOut","examples":[{"checked_at":1788000412.0,"domain":"acme.io","email":"jane.doe@acme.io","is_catch_all":false,"is_disposable":false,"is_free":false,"is_role":false,"job_filename":"list.csv","job_id":"j_4c19be","list_type":"Cold outreach","mx_found":true,"score":96,"settled_at":6,"status":"valid","sub_status":"mailbox_confirmed","verdict":"deliverable"}]},"AddressPage":{"properties":{"rows":{"items":{"$ref":"#/components/schemas/AddressOut"},"type":"array","title":"Rows","description":"This page of addresses."},"total":{"type":"integer","title":"Total","description":"Addresses matching the filter, all pages."},"page":{"type":"integer","title":"Page","description":"The 1-based page returned."},"size":{"type":"integer","title":"Size","description":"Rows per page."}},"type":"object","required":["rows","total","page","size"],"title":"AddressPage","examples":[{"page":1,"rows":[{"checked_at":1788000412.0,"domain":"acme.io","email":"jane.doe@acme.io","is_catch_all":false,"is_disposable":false,"is_free":false,"is_role":false,"job_filename":"list.csv","job_id":"j_4c19be","list_type":"Cold outreach","mx_found":true,"score":96,"settled_at":6,"status":"valid","sub_status":"mailbox_confirmed","verdict":"deliverable"}],"size":50,"total":4812}]},"Body_upload_file_v1_files_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_file_v1_files_post"},"ColumnDetection":{"properties":{"columns":{"items":{"type":"string"},"type":"array","title":"Columns","description":"The header row, in file order."},"sample_rows":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Sample Rows","description":"The first few rows, for confirming the mapping."},"guessed_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Guessed Email","description":"The column that most likely holds the address."},"guessed_first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Guessed First Name","description":"A likely first-name column."},"guessed_last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Guessed Last Name","description":"A likely last-name column."},"delimiter":{"type":"string","title":"Delimiter","description":"The delimiter found in the file. The output is written back with this one.","examples":[","]}},"type":"object","required":["columns","sample_rows","delimiter"],"title":"ColumnDetection","examples":[{"columns":["email","company","signed_up"],"delimiter":",","guessed_email":"email","sample_rows":[{"company":"Acme","email":"jane.doe@acme.io","signed_up":"2026-01-14"}]}]},"ColumnMappingIn":{"properties":{"email":{"type":"string","title":"Email","description":"The column holding the address. The only mapping the engine needs; every other column is carried through untouched."},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name","description":"Optional first-name column."},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name","description":"Optional last-name column."}},"type":"object","required":["email"],"title":"ColumnMappingIn"},"CreateJobRequest":{"properties":{"file_id":{"type":"string","title":"File Id","description":"A file_id returned by POST /v1/files."},"mapping":{"$ref":"#/components/schemas/ColumnMappingIn","description":"Which column holds the address."},"webhook_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Url","description":"Called once when the run finishes. Signed with HMAC-SHA256 when a webhook secret is configured."},"filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Filename","description":"The name to show in History. Defaults to the storage key."},"list_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Type","description":"The list this run belongs to, for slicing the workspace."}},"type":"object","required":["file_id","mapping"],"title":"CreateJobRequest","examples":[{"file_id":"f_8s21kd_list.csv","filename":"list.csv","mapping":{"email":"email"}}]},"ExportRequest":{"properties":{"verdicts":{"items":{"type":"string"},"type":"array","title":"Verdicts","description":"Any of the four primary verdicts. Empty means all of them."},"search":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search","description":"Substring match on the address or its domain."},"list_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"List Type","description":"Restrict to one list."},"preset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preset","description":"A slice defined by a sub-reason rather than a verdict: \"catchall\" or \"disposable\"."},"emails":{"items":{"type":"string"},"type":"array","title":"Emails","description":"Only these addresses, for exporting a hand-picked selection."},"columns":{"type":"string","title":"Columns","description":"\"full\" carries the verdict, sub-reason, score, MX and settling layer; \"email\" is one column.","default":"full"},"require_mx":{"type":"boolean","title":"Require Mx","description":"Drop rows whose domain has no MX.","default":false},"exclude_disposable":{"type":"boolean","title":"Exclude Disposable","description":"Drop disposable domains.","default":false}},"type":"object","title":"ExportRequest","examples":[{"columns":"email","exclude_disposable":true,"verdicts":["undeliverable"]}]},"FileUploadResponse":{"properties":{"file_id":{"type":"string","title":"File Id","description":"The storage key to pass to POST /v1/jobs."},"filename":{"type":"string","title":"Filename","description":"The name the file was uploaded under."},"detection":{"$ref":"#/components/schemas/ColumnDetection","description":"What the header sniffer found, for mapping."}},"type":"object","required":["file_id","filename","detection"],"title":"FileUploadResponse","examples":[{"detection":{"columns":["email","company","signed_up"],"delimiter":",","guessed_email":"email","sample_rows":[{"company":"Acme","email":"jane.doe@acme.io","signed_up":"2026-01-14"}]},"file_id":"f_8s21kd_list.csv","filename":"list.csv"}]},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","title":"Status","description":"\"ok\" while the process is serving."},"version":{"type":"string","title":"Version","description":"The engine version answering."},"smtp_enabled":{"type":"boolean","title":"Smtp Enabled","description":"Whether the mailbox probe is running on this engine."},"dns_enabled":{"type":"boolean","title":"Dns Enabled","description":"Whether DNS and MX resolution is running on this engine."}},"type":"object","required":["status","version","smtp_enabled","dns_enabled"],"title":"HealthResponse","examples":[{"dns_enabled":true,"smtp_enabled":true,"status":"ok","version":"0.1.0"}]},"JobResponse":{"properties":{"id":{"type":"string","title":"Id","description":"The job id, used on every other job route."},"seq":{"type":"integer","title":"Seq","description":"This workspace's own run number.","default":0},"filename":{"type":"string","title":"Filename","description":"The file the run was started from."},"status":{"type":"string","title":"Status","description":"queued, running, done or failed.","examples":["running"]},"list_type":{"type":"string","title":"List Type","description":"The list this run belongs to.","default":"Imports"},"counts":{"additionalProperties":{"type":"integer"},"type":"object","title":"Counts","description":"Rows settled, keyed by verdict."},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Why the run failed, when it did."},"outputs":{"items":{"type":"string"},"type":"array","title":"Outputs","description":"The segments ready to download: cleaned, valid, removed."},"mapping":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Mapping","description":"The column mapping the run was started with."},"phase":{"type":"string","title":"Phase","description":"Which stage of the run is executing.","default":"queued"},"processed":{"type":"integer","title":"Processed","description":"Rows settled so far.","default":0},"total":{"type":"integer","title":"Total","description":"Rows in the file.","default":0},"domains_total":{"type":"integer","title":"Domains Total","description":"Distinct domains in the file.","default":0},"progress":{"type":"number","title":"Progress","description":"Fraction complete, 0 to 1.","default":0.0},"created_at":{"type":"number","title":"Created At","description":"Unix time the job was accepted.","default":0.0},"started_at":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Started At","description":"Unix time work began."},"finished_at":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Finished At","description":"Unix time work ended."},"duration":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Duration","description":"Seconds the run took."}},"type":"object","required":["id","filename","status","counts","outputs"],"title":"JobResponse","examples":[{"counts":{"deliverable":1840,"risky":402,"undeliverable":297,"unknown":311},"created_at":1788000000.0,"domains_total":731,"filename":"list.csv","id":"j_4c19be","list_type":"Cold outreach","mapping":{"email":"email"},"outputs":[],"phase":"smtp","processed":2850,"progress":0.59,"seq":12,"started_at":1788000004.0,"status":"running","total":4812}]},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyRequest":{"properties":{"email":{"type":"string","maxLength":320,"minLength":1,"title":"Email","description":"The address to verify. Parsed, normalised and probed as sent.","examples":["john.doe@gmail.com"]},"check_dns":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Check Dns","description":"Override the engine's DNS setting for this call. With DNS off, nothing past layer 3 can run and the verdict comes back Unknown."},"check_smtp":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Check Smtp","description":"Override the engine's SMTP setting for this call. With the probe off, a deliverable-looking address is still reported Unknown rather than guessed at."}},"type":"object","required":["email"],"title":"VerifyRequest"},"VerifyResponse":{"properties":{"email":{"type":"string","title":"Email","description":"The address exactly as it was sent."},"status":{"type":"string","title":"Status","description":"The verdict: deliverable, risky, unknown or undeliverable.","examples":["undeliverable"]},"sub_status":{"type":"string","title":"Sub Status","description":"The reason under the verdict, such as no_mx, role_account or catch_all.","examples":["no_mx"]},"score":{"type":"integer","title":"Score","description":"Confidence in the verdict, 0 to 100."},"normalized_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Normalized Email","description":"The address with provider-specific spelling collapsed: Gmail dots and plus tags removed, case folded."},"dedupe_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dedupe Key","description":"The key two spellings of one mailbox share. Count this, not the address, to count people."},"domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Domain","description":"The domain part, lower-cased."},"suggested_correction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Suggested Correction","description":"A likely intended address when the domain looks misspelled. A suggestion, never an automatic substitution.","examples":["john.doe@gmail.com"]},"is_disposable":{"type":"boolean","title":"Is Disposable","description":"The domain is on the vendored disposable list."},"is_role":{"type":"boolean","title":"Is Role","description":"The local part reaches a desk rather than a person."},"is_free":{"type":"boolean","title":"Is Free","description":"The domain is a free consumer provider."},"is_catch_all":{"type":"boolean","title":"Is Catch All","description":"The domain accepts every recipient, so its acceptance of this one proves nothing."},"mx_found":{"type":"boolean","title":"Mx Found","description":"The domain published usable MX records."},"tags":{"items":{"type":"string"},"type":"array","title":"Tags","description":"Flags raised along the way, as a list."},"checks":{"additionalProperties":true,"type":"object","title":"Checks","description":"One entry per layer that ran, in order, with what it returned. This is the trace behind the verdict."}},"type":"object","required":["email","status","sub_status","score","is_disposable","is_role","is_free","is_catch_all","mx_found","tags","checks"],"title":"VerifyResponse","examples":[{"checks":{"mx":"none","normalize":"ok","syntax":"ok","typo":"gmial.com -> gmail.com"},"dedupe_key":"john@gmial.com","domain":"gmial.com","email":"john@gmial.com","is_catch_all":false,"is_disposable":false,"is_free":false,"is_role":false,"mx_found":false,"normalized_email":"john@gmial.com","score":4,"status":"undeliverable","sub_status":"no_mx","suggested_correction":"john@gmail.com","tags":["typo_suspected"]}]}}}}