{"openapi":"3.1.0","info":{"title":"FairValid Customer API","version":"v1","description":"Server-side API for scoped live and simulated test keys. Live verification currently returns 503 while its charging gates remain closed. A queued live job uses the same list manifest, results and credits as the dashboard."},"servers":[{"url":"/","description":"Your FairValid application origin"}],"tags":[{"name":"Jobs","description":"Asynchronous bulk and single-email verification"},{"name":"Balance","description":"Read-only workspace credits for live keys"},{"name":"Simulator","description":"Free deterministic test-key responses. Bulk sandbox jobs and replay records are retained for 30 days; use a new idempotency key after that window."}],"components":{"securitySchemes":{"FairValidKey":{"type":"http","scheme":"bearer","bearerFormat":"fv_live_ or fv_test_ server-side key","description":"Use one scoped key in Authorization. Secrets appear once at creation; never put them in a browser or mobile app."}},"parameters":{"JobId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":true,"description":"8–128 allowed characters. Retrying the same key and normalized body returns the original job; a changed body returns 409. Use a new key only for a deliberate fresh run.","schema":{"type":"string","minLength":8,"maxLength":128,"pattern":"^[A-Za-z0-9._:-]+$"}}},"schemas":{"JobInput":{"type":"object","additionalProperties":false,"required":["emails"],"properties":{"emails":{"type":"array","minItems":1,"maxItems":5000,"items":{"type":"string","format":"email"},"description":"Duplicate normalized addresses count once toward the job and key cap."},"name":{"type":"string","minLength":1,"maxLength":180,"default":"API batch"}}},"SingleInput":{"type":"object","additionalProperties":false,"required":["email"],"properties":{"email":{"type":"string","format":"email"},"mode":{"type":"string","enum":["live","sandbox"]}}},"JobAccepted":{"type":"object","required":["version","brand","mode","job_id","status","unique_addresses","results_url"],"properties":{"version":{"const":"v1"},"brand":{"const":"FairValid"},"mode":{"type":"string","enum":["live","sandbox"]},"job_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","running","paused","canceling","canceled","completed","pending","failed","cancelled"]},"unique_addresses":{"type":"integer","minimum":1},"results_url":{"type":"string"},"charged_credits":{"type":"integer","const":0,"description":"Present for sandbox jobs only."},"provider_submitted":{"type":"boolean","const":false,"description":"Present for sandbox jobs only."}}},"JobStatus":{"type":"object","required":["version","brand","mode","job_id","status","unique_addresses","summary","results_url"],"properties":{"version":{"const":"v1"},"brand":{"const":"FairValid"},"mode":{"type":"string","enum":["live","sandbox"]},"job_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"updated_at":{"type":"string","format":"date-time"},"unique_addresses":{"type":"integer"},"summary":{"type":"object","additionalProperties":{"type":"integer"}},"results_url":{"type":"string"}}},"Results":{"type":"object","required":["version","brand","mode","job_id","items","matching_count","next_cursor"],"properties":{"version":{"const":"v1"},"brand":{"const":"FairValid"},"mode":{"type":"string","enum":["live","sandbox"]},"job_id":{"type":"string","format":"uuid"},"items":{"type":"array","maxItems":50,"items":{"type":"object","required":["email","result"],"properties":{"email":{"type":"string"},"result":{"type":"string"},"credit_status":{"type":["string","null"]},"checked_at":{"type":["string","null"],"format":"date-time"}}}},"matching_count":{"type":"integer"},"next_cursor":{"type":["string","null"]}}},"Balance":{"type":"object","required":["version","brand","mode","available_credits","held_credits","reversal_debt"],"properties":{"version":{"const":"v1"},"brand":{"const":"FairValid"},"mode":{"const":"live"},"available_credits":{"type":"integer"},"held_credits":{"type":"integer"},"reversal_debt":{"type":"integer"},"free_available":{"type":"integer"},"paid_available":{"type":"integer"},"free_period_ends_at":{"type":["string","null"],"format":"date-time"}}},"Error":{"type":"object","required":["version","brand","error","request_id"],"properties":{"version":{"const":"v1"},"brand":{"const":"FairValid"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}},"request_id":{"type":"string","format":"uuid"}}},"WebhookEvent":{"type":"object","required":["id","type","api_version","mode","created_at","data"],"properties":{"id":{"type":"string","pattern":"^evt_"},"type":{"type":"string","enum":["job.completed","job.canceled","test.ping"]},"api_version":{"const":"v1"},"mode":{"type":"string","enum":["live","test"]},"created_at":{"type":"string","format":"date-time"},"data":{"type":"object","description":"Job events include job_id, status, unique_addresses, summary, credits and results_url. Test ping includes only a message. No email addresses or provider data are sent."}}}},"responses":{"Unauthorized":{"description":"Missing, revoked or insufficiently scoped key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unavailable":{"description":"Temporary unavailability; retry with the same Idempotency-Key if a submission outcome is uncertain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"security":[{"FairValidKey":[]}],"paths":{"/api/v1/jobs":{"post":{"tags":["Jobs"],"operationId":"submitJob","summary":"Submit a bulk job","description":"JSON body limit 2 MiB. Live keys need submit scope, and live admission gates must be open. Test keys accept only documented @example.com scenarios and never call a provider or consume credits. A live key cap counts charged plus reserved unique identities; a cap does not mint workspace credits.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobInput"},"examples":{"sandbox":{"value":{"emails":["deliverable@example.com","pending@example.com"]}}}}}},"responses":{"200":{"description":"Identical replay returned the original job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"}}}},"202":{"description":"New job accepted; no credit is held until live admission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"}}}},"400":{"description":"Invalid JSON or missing Idempotency-Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"key_cap_reached; no job or credit hold was created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Idempotency-Key reused with a different normalized request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Invalid addresses or list limits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Sandbox workspace reached 20 new jobs per hour or 100 per 30 days; identical replays remain available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/api/v1/verifications":{"post":{"tags":["Simulator","Jobs"],"operationId":"submitSingle","summary":"Submit one email","description":"A live key submits an asynchronous one-address job and requires Idempotency-Key. A test key uses the existing deterministic single-email simulator with reserved addresses, no Idempotency-Key, no credits and no provider calls. The legacy single simulator response uses version 2026-10-01 and camelCase fields.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SingleInput"},"example":{"email":"deliverable@example.com"}}}},"responses":{"200":{"description":"Simulated outcome or identical live replay"},"202":{"description":"Pending simulator scenario or new live job"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"description":"Invalid email or sandbox scenario"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/api/v1/jobs/{id}":{"get":{"tags":["Jobs"],"operationId":"getJob","summary":"Read job status","description":"A live key needs read scope and may read only jobs in its workspace. Test keys read only jobs created by that test key.","parameters":[{"$ref":"#/components/parameters/JobId"}],"responses":{"200":{"description":"Current job state and dashboard-equivalent totals","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatus"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Job not found in this key's scope"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/api/v1/jobs/{id}/results":{"get":{"tags":["Jobs"],"operationId":"getJobResults","summary":"Read result page","description":"50 items per page; at most 2,000 pages. Use next_cursor unchanged for the next page. Results and credit_status follow the dashboard snapshot for live jobs.","parameters":[{"$ref":"#/components/parameters/JobId"},{"name":"filter","in":"query","schema":{"type":"string","enum":["all","valid","invalid","catch_all","unknown","still_checking","not_checked"]}},{"name":"search","in":"query","schema":{"type":"string","maxLength":254}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Signed continuation token; includes the original filter and search."}],"responses":{"200":{"description":"Selected result page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Results"}}}},"400":{"description":"Invalid cursor or filter"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Job not found"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/api/v1/jobs/{id}/cancel":{"post":{"tags":["Jobs"],"operationId":"cancelJob","summary":"Request job cancellation","description":"Live jobs use the dashboard cancellation workflow; already submitted items may finish. Test pending jobs switch to cancelled without a provider call.","parameters":[{"$ref":"#/components/parameters/JobId"}],"responses":{"200":{"description":"Test job cancelled or already terminal"},"202":{"description":"Live cancellation request accepted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Job not found"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/api/v1/balance":{"get":{"tags":["Balance"],"operationId":"getBalance","summary":"Read workspace credit balance","description":"Live keys with balance scope only. Available and held balances are the same account used by the dashboard. A key cap is separate from workspace credits.","responses":{"200":{"description":"Current workspace balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"503":{"$ref":"#/components/responses/Unavailable"}}}}},"webhooks":{"jobEvent":{"post":{"summary":"Job completion, cancellation or test ping","description":"HTTPS POST to the configured destination. Event ID is stable across retries. Accept 2xx within 10 seconds. Up to eight automatic attempts with backoff; history remains 30 days. Resolve the destination anew and refuse private addresses or redirects on every attempt. Current secrets sign new attempts; keep the previous secret for 24 hours after rotation to verify in-flight signatures. Deduplicate by FairValid-Webhook-Id.","parameters":[{"name":"FairValid-Webhook-Id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"FairValid-Webhook-Timestamp","in":"header","required":true,"schema":{"type":"string","pattern":"^[0-9]+$"}},{"name":"FairValid-Webhook-Signature","in":"header","required":true,"description":"fv1=<lowercase HMAC-SHA256 hex> over UTF-8 bytes fv1\\n<event UUID>\\n<Unix seconds>\\n<raw JSON body>. Reject a timestamp more than five minutes from local time. Compare the MAC in constant time.","schema":{"type":"string","pattern":"^fv1=[0-9a-f]{64}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"204":{"description":"Accepted"}}}}}}