{"openapi":"3.1.0","info":{"title":"Bounceless API","version":"4.1.0","description":"GA single and batch email verification. Unknown outcomes are not billed and credits never expire.","x-bounceless-presend-contract-version":1,"x-bounceless-compatibility":"Legacy sibling fields remain available during the v1 compatibility window."},"servers":[{"url":"https://api.bounceless.io"}],"security":[{"apiKey":[]}],"paths":{"/v1/verify":{"post":{"summary":"Verify one email address.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyInput"}}}},"responses":{"200":{"description":"Completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResult"}}}},"202":{"description":"The bounded synchronous wait ended before a verdict. When async results are enabled for the account, the request is persisted: the response carries verifyRequestId, the credit hold stays held, and GET /v1/verify/{id} returns the result (billed once, at the first terminal result; non-verdicts are not billed). A replay with the same Idempotency-Key answers from that stored request without a new verification; the same key with another address domain returns 409 idempotency_key_reused. Otherwise no verifyRequestId is returned, no credits are charged and the hold is released: retry POST /v1/verify with the same Idempotency-Key and body, using a bounded client retry policy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyPendingResult"}}}},"400":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"Retry-After":{"description":"Seconds before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/verify/{id}":{"get":{"summary":"Get a single verification result.","description":"Poll the verifyRequestId returned by a 202 POST /v1/verify (for example every 2s under a client deadline) until status is completed or failed. running keeps the hold held and bills nothing; the first terminal result settles the charge once, and later reads return the stored result unchanged. Unknown ids, ids of another account, and accounts without async results enabled return 404 not_found.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Current result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyRequestResult"}}}},"401":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"Retry-After":{"description":"Seconds before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/requests":{"post":{"summary":"Create a batch verification request.","description":"Creation returns 202. A replay with the same Idempotency-Key and body returns 200 with the same request.id.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchInput"}}}},"responses":{"200":{"description":"Idempotent replay.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchRequest"}}}},"202":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchRequest"}}}},"400":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"Retry-After":{"description":"Seconds before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/requests/{id}":{"get":{"summary":"Get a batch verification request.","description":"Poll with bounded exponential backoff until request.state is terminal. finalized is the successful export-ready terminal. failed_validation, rejected_credit, cancelled, failed, expired, and purged are also terminal and require client handling; completed is transitional.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Request status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchRequest"}}}},"401":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"Retry-After":{"description":"Seconds before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/requests/{id}/results":{"get":{"summary":"Get batch verification results.","description":"Results are readable while processing. Prefer cursor pagination: start without cursor, then pass each non-null nextCursor unchanged. Keep collecting while countsMayChange is true; after the request becomes terminal, start one final cursor walk from the beginning and de-duplicate by email to obtain the settled snapshot. offset remains supported for compatibility but is not recommended. A malformed cursor returns 400. Optional state, resultCategory and q filters narrow the walk server-side before pagination; keep them unchanged while following nextCursor. state preserves the initial outcome; resultCategory groups completed catch-all resolutions into deliverable, undeliverable or risky. Unknown filter values or a q longer than 200 characters return 400.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Offset"},{"$ref":"#/components/parameters/ResultState"},{"$ref":"#/components/parameters/ResultCategory"},{"$ref":"#/components/parameters/ResultSearch"},{"$ref":"#/components/parameters/Order"}],"responses":{"200":{"description":"Paginated results.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResultsPage"}}}},"400":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Error response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"Retry-After":{"description":"Seconds before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"Bounceless key in blc.… format."}},"headers":{"RequestId":{"description":"Request correlation identifier.","schema":{"type":"string"}}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":1}},"Limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},"Cursor":{"name":"cursor","in":"query","description":"Opaque continuation token returned as nextCursor. Do not decode or modify it. When present, cursor takes precedence over offset.","schema":{"type":"string","minLength":1}},"ResultState":{"name":"state","in":"query","description":"Comma-separated list of presented row states (the state field of each result). recheck selects retry-exhausted rows, which never match their stored state.","schema":{"type":"string","minLength":1},"example":"valid,catch_all"},"ResultCategory":{"name":"resultCategory","in":"query","description":"Comma-separated final result categories: deliverable, catch_all, unknown, undeliverable, risky, recheck, pending, other. Combines with state and literal q; unresolved, pending or failed catch-all resolutions remain catch_all.","schema":{"type":"string","minLength":1},"example":"deliverable"},"ResultSearch":{"name":"q","in":"query","description":"Case-insensitive literal substring of the address.","schema":{"type":"string","maxLength":200}},"Offset":{"name":"offset","in":"query","deprecated":true,"description":"Compatibility pagination. Prefer cursor/nextCursor.","schema":{"type":"integer","minimum":0,"default":0}},"Order":{"name":"order","in":"query","description":"Row order. asc (default) is oldest-first and is the only order cursor pagination walks. desc returns the newest rows first as a live tail read: nextCursor is always null and combining it with cursor returns 400.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"}}},"schemas":{"Error":{"type":"object","required":["error"],"additionalProperties":false,"properties":{"error":{"type":"object","required":["code","message","requestId"],"additionalProperties":false,"properties":{"code":{"type":"string"},"message":{"type":"string"},"requestId":{"type":"string"}}}}},"VerifyInput":{"type":"object","required":["email"],"additionalProperties":false,"properties":{"email":{"type":"string","format":"email","maxLength":320},"timeoutMs":{"type":"integer","minimum":1,"maximum":120000}}},"VerifyResult":{"type":"object","required":["status","requestId"],"properties":{"status":{"type":"string","const":"completed"},"resultCategory":{"type":"string","enum":["deliverable","catch_all","unknown","undeliverable","risky","recheck","pending","other"],"description":"Observed result category, independent of the recommendation and billing."},"verifyRequestId":{"type":"string","format":"uuid","description":"Present when async results are enabled for the account."},"createdAt":{"type":"string","format":"date-time"},"decision":{"type":"object"},"presend":{"$ref":"#/components/schemas/ClientDecisionEnvelopeV1"},"evidence":{"$ref":"#/components/schemas/ControlEvidenceList"},"checks":{"$ref":"#/components/schemas/Checks"},"isRole":{"type":["boolean","null"],"description":"Engine role label (BL-11811). Not a check, never gating; null when the engine did not emit it."},"engine":{"$ref":"#/components/schemas/VerifyEngine"},"requestId":{"type":"string"}}},"VerifyPendingResult":{"type":"object","required":["status","engineJobId","presend","requestId"],"properties":{"status":{"type":"string","enum":["queued","running"]},"resultCategory":{"type":"string","enum":["deliverable","catch_all","unknown","undeliverable","risky","recheck","pending","other"]},"verifyRequestId":{"type":"string","format":"uuid","description":"Present when async results are enabled for the account; poll GET /v1/verify/{id}."},"engineJobId":{"type":"string"},"presend":{"$ref":"#/components/schemas/ClientDecisionEnvelopeV1"},"requestId":{"type":"string"}}},"VerifyRequestResult":{"type":"object","required":["status","presend","requestId"],"properties":{"status":{"type":"string","enum":["running","completed","failed"]},"resultCategory":{"type":"string","enum":["deliverable","catch_all","unknown","undeliverable","risky","recheck","pending","other"]},"verifyRequestId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"decision":{"type":["object","null"]},"presend":{"$ref":"#/components/schemas/ClientDecisionEnvelopeV1"},"evidence":{"$ref":"#/components/schemas/ControlEvidenceList"},"checks":{"$ref":"#/components/schemas/Checks"},"engine":{"$ref":"#/components/schemas/VerifyEngine"},"isRole":{"type":["boolean","null"],"description":"Engine role label (BL-11811). Not a check, never gating; null when the engine did not emit it."},"requestId":{"type":"string"}}},"VerifyEngine":{"type":"object","properties":{"verifiedAt":{"type":["string","null"]},"attempts":{"type":["integer","null"]},"providerFamily":{"type":["string","null"]}}},"BatchInput":{"type":"object","required":["emails"],"additionalProperties":false,"properties":{"emails":{"type":"array","minItems":1,"maxItems":10000,"items":{"type":"string","format":"email","maxLength":320}}}},"RequestDescriptor":{"type":"object","required":["id","mode","state","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"mode":{"type":"string"},"state":{"type":"string","enum":["received","validating","queued","running","retrying","degraded","paused","completed","finalized","failed_validation","rejected_credit","cancelled","failed","expired","purged"]},"partial":{"type":"boolean"},"finalized":{"type":"boolean"},"countsMayChange":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}}},"Counters":{"type":"object","additionalProperties":{"type":"integer","minimum":0}},"BatchRequest":{"type":"object","required":["request","counters","requestId"],"properties":{"request":{"$ref":"#/components/schemas/RequestDescriptor"},"counters":{"$ref":"#/components/schemas/Counters"},"resultCounters":{"$ref":"#/components/schemas/ResultCounters"},"summary":{"$ref":"#/components/schemas/RequestSummary"},"replayed":{"type":"boolean"},"requestId":{"type":"string"}}},"ResultCounters":{"type":"object","description":"Whole-list final result categories, returned by request detail; independent of search and pagination. Existing counters retain first-pass semantics.","properties":{"submittedCount":{"type":"integer","minimum":0},"totalCount":{"type":"integer","minimum":0},"validCount":{"type":"integer","minimum":0},"catchAllCount":{"type":"integer","minimum":0},"unknownCount":{"type":"integer","minimum":0},"invalidCount":{"type":"integer","minimum":0},"riskyCount":{"type":"integer","minimum":0},"recheckCount":{"type":"integer","minimum":0},"pendingCount":{"type":"integer","minimum":0},"otherCount":{"type":"integer","minimum":0}}},"RequestSummary":{"type":"object","description":"Full-request aggregates, returned by GET /v1/requests/{id}. deliverableRate is omitted when no address has resolved.","required":["checkedCount","deliverableCount","recheckCount"],"additionalProperties":false,"properties":{"checkedCount":{"type":"integer","minimum":0,"description":"Resolved addresses: valid, invalid, risky, catch_all and unknown."},"deliverableCount":{"type":"integer","minimum":0},"deliverableRate":{"type":"number","minimum":0,"maximum":1,"description":"deliverableCount / checkedCount as a ratio, 4 decimals."},"recheckCount":{"type":"integer","minimum":0,"description":"Rows across the whole request requiring another check (deferred or recheck_scheduled)."}}},"ResultItem":{"type":"object","properties":{"email":{"type":"string"},"state":{"type":"string"},"resultCategory":{"type":"string","enum":["deliverable","catch_all","unknown","undeliverable","risky","recheck","pending","other"]},"score":{"type":["number","null"]},"subStatus":{"type":["string","null"]},"reasonCodes":{"type":["array","null"],"items":{"type":"string"}},"decision":{"type":["object","null"]},"presend":{"type":"object"},"evidence":{"$ref":"#/components/schemas/ControlEvidenceList"},"checks":{"$ref":"#/components/schemas/Checks"}}},"ResultsPage":{"type":"object","required":["jobId","results","limit","offset","nextCursor","partial","finalized","countsMayChange","requestId"],"properties":{"jobId":{"type":"string","format":"uuid"},"results":{"type":"array","items":{"$ref":"#/components/schemas/ResultItem"}},"limit":{"type":"integer","minimum":1,"maximum":200},"offset":{"type":"integer","minimum":0,"description":"Compatibility echo; ignore when traversing with cursor."},"nextCursor":{"type":["string","null"],"description":"Pass a non-null value as the next cursor query parameter. null terminates this walk."},"partial":{"type":"boolean","description":"true until a successful finalized snapshot exists."},"finalized":{"type":"boolean"},"countsMayChange":{"type":"boolean","description":"When true, more rows or changed counters may appear while the job runs."},"requestId":{"type":"string"}}},"V2State":{"type":"string","enum":["DELIVERABLE","UNDELIVERABLE_NO_MAILBOX","UNDELIVERABLE_NO_MAIL_SERVER","UNDELIVERABLE_BOUNCED","CATCH_ALL","MAILBOX_FULL","DISPOSABLE","ROLE","SPAMTRAP","RISKY_SPAMTRAP","BLACKLISTED","GREYLISTED","TRANSIENT","UNKNOWN","UNKNOWN_WEBMAIL","REDIRECT"]},"ControlEvidenceList":{"type":"array","description":"Optional per-check evidence (BL-11576). Present only for checks the engine actually reported; the key is omitted when none were. A missing check means unavailable, never pass. Never derived from the verdict.","items":{"$ref":"#/components/schemas/ControlEvidence"}},"Checks":{"type":"object","additionalProperties":false,"required":["syntax","mail_records","mailbox","risk"],"description":"Per-control checks (BL-11693, engine wire BL-11692). Always four keys. States are the engine's, passed through; a check the engine did not emit is {state:\"unknown\", reason_code:\"evidence_unavailable\"}. Never derived from the verdict.","properties":{"syntax":{"$ref":"#/components/schemas/Check"},"mail_records":{"$ref":"#/components/schemas/Check"},"mailbox":{"$ref":"#/components/schemas/Check"},"risk":{"$ref":"#/components/schemas/Check"}}},"Check":{"type":"object","additionalProperties":false,"required":["state","reason_code"],"properties":{"state":{"type":"string","enum":["passed","failed","unknown","not_run","error"]},"reason_code":{"type":"string","description":"Machine token; evidence_unavailable when the engine emitted nothing usable."},"observed_at":{"type":"string","format":"date-time"},"detail":{"type":"string","description":"Machine token only; free text is never forwarded."}}},"ControlEvidence":{"type":"object","required":["kind","state","reason","observedAt","provenance"],"properties":{"kind":{"type":"string","enum":["format","mx","mailbox","catch_all","risk"]},"state":{"type":"string","enum":["pass","fail","unknown","not_run"],"description":"Taken from the engine classification of that check (SMTP matched rule, catch-all reason), never the raw SMTP code class. unknown = ran, inconclusive (incl. greylist, or a block of our sending IP); not_run = engine attested it did not run; fail = ran with an unfavourable observation."},"reason":{"type":"string","description":"Stable machine token, e.g. rcpt_accepted, rcpt_rejected, mailbox_full, rcpt_inconclusive, probe_not_attempted, catch_all_detected, catch_all_ruled_out, catch_all_unconfirmed, list_match."},"observedAt":{"type":["string","null"],"format":"date-time","description":"Engine observation instant (cycle attestation, else engine verified_at). null when the engine did not provide one."},"provenance":{"type":["object","null"],"required":["mechanism","mechanismRevision","freshness"],"properties":{"mechanism":{"type":["string","null"]},"mechanismRevision":{"type":"string"},"freshness":{"type":"string","enum":["fresh_probe","fresh_cache"]}}},"measurement":{"type":"object","additionalProperties":{"type":["string","number","boolean","null"]},"description":"Allowlisted measurements (smtpCode, enhancedCode, probes, accepts, quorum, list)."}}},"ClientDecisionEnvelopeV1":{"type":"object","additionalProperties":false,"required":["contract_version","status","outcome","decision","verdict","provider_family","confidence","reasons","evidence_class","retry","billing","history"],"properties":{"contract_version":{"type":"integer","const":1},"status":{"type":"string","enum":["running","completed","deferred","recheck_scheduled","failed","rejected"]},"outcome":{"type":"string","enum":["pending","actionable_verdict","non_verdict"]},"decision":{"oneOf":[{"type":"string","enum":["send","suppress","send_with_caution","enrich_first","recheck_later","manual_review"]},{"type":"null"}]},"verdict":{"oneOf":[{"type":"string","enum":["DELIVERABLE","UNDELIVERABLE_NO_MAILBOX","UNDELIVERABLE_NO_MAIL_SERVER","UNDELIVERABLE_BOUNCED","CATCH_ALL","MAILBOX_FULL","DISPOSABLE","ROLE","SPAMTRAP","RISKY_SPAMTRAP","BLACKLISTED","GREYLISTED","TRANSIENT","UNKNOWN","UNKNOWN_WEBMAIL"]},{"type":"null"}]},"provider_family":{"type":["string","null"]},"confidence":{"type":["number","null"],"minimum":0,"maximum":1},"reasons":{"type":"array","items":{"type":"string"}},"evidence_class":{"type":["string","null"]},"retry":{"oneOf":[{"type":"object","additionalProperties":false,"properties":{"eligible_at":{"type":"string"},"expires_at":{"type":"string"},"affinity_required":{"type":"boolean"}}},{"type":"null"}]},"billing":{"type":"object","additionalProperties":false,"required":["disposition","credits"],"properties":{"disposition":{"type":"string","enum":["held","debited","released"]},"credits":{"type":["integer","null"],"minimum":0,"description":"Actually settled normal verification credits; null when exact historical row attribution is unavailable. New standard jobs include catch-all resolution with no supplemental debit; historical jobs retain their accepted quote. Catch-all charges are reported separately."}}},"history":{"oneOf":[{"type":"object"},{"type":"null"}]}}}}}}