{
    "openapi": "3.1.0",
    "info": {
        "title": "Spaw API",
        "version": "1.0.0",
        "summary": "Email verification API with one key, one credit balance, and typed JSON answers that name their data sources.",
        "description": "Spaw verifies email addresses over plain REST: JSON in and out, typed errors, and one credit balance across every endpoint. Every lookup runs RFC syntax parsing, live DNS and MX checks, SPF and DMARC records, open disposable and role lists, and a mailbox-level SMTP handshake. No verification ever sends mail.\n\n## Authentication\n\nEvery request carries a secret API key as a bearer token. Create and revoke keys on the dashboard's API keys page; a key is shown once at creation and stored hashed.\n\n```http\nAuthorization: Bearer sk_live_…\nContent-Type: application/json\n```\n\nThere are two kinds of keys. Secret `sk_live_` keys authenticate servers and unlock the full API. Publishable `pk_` keys are safe in browser page source: they can only run single email lookups through `POST /api/v1/email/public`, and only from the domains they are locked to.\n\n## Response envelope\n\nSuccessful lookups answer `{ \"success\": true, \"data\": …, \"meta\": … }`. The `meta` block carries `credits_used`, `credits_remaining`, `cache_hit`, and `request_id` (email lookups add `suppressed`, and `settling` when a callback was requested). Failures answer `{ \"success\": false, \"error\": { \"code\", \"message\", \"request_id\" } }` with a stable code to branch on; validation failures add `error.errors` keyed by field. Every response also carries an `X-Request-Id` header that matches every log line the request produced. Quote it when writing to support.\n\n## Billing\n\nYou pay for answers, not attempts, and anything you paid to check once is free to check again for seven days.\n\n| Outcome | Credits |\n| --- | --- |\n| Fresh lookup with a usable answer (deliverable or risky verdict, a domain that accepts mail, a valid phone number, a located IP) | 1 |\n| Invalid input or an undeliverable verdict | 0 |\n| Repeat of a recent lookup, within 7 days | 0 |\n| Rejected or failed request | 0 |\n| Monthly free grant, every account | +10 |\n\nCredit packs are one-time purchases from the dashboard's Billing page (from $9 for 1,000 credits) and never expire. `GET /api/v1/account` returns the live balance for pre-flight checks.\n\n## Rate limits\n\nEvery key may make 5 requests per second; a batch or bulk call counts as one request. Over the limit, the API answers `429 RATE_LIMITED` with a `Retry-After` header.\n\n## Test addresses\n\nSix fixed addresses on the reserved `spaw.test` domain always answer the same canonical result, cost no credits, touch no DNS or mail server, and never appear in your history or suppression list. A plus-tag (`deliverable+ci@spaw.test`) works too, on every endpoint including batch, bulk, and the browser endpoint.\n\n| Address | Answer |\n| --- | --- |\n| deliverable@spaw.test | deliverable · mailbox confirmed, risk_score 0 |\n| unverified@spaw.test | deliverable · smtp_checked false, smtp_reason greylisted, mailbox_confidence set |\n| catch-all@spaw.test | risky · reason catch_all, mailbox_exists null |\n| disposable@spaw.test | risky · reason disposable, risk_score 90 |\n| role@spaw.test | risky · reason role |\n| undeliverable@spaw.test | undeliverable · reason mailbox_not_found, smtp_reason no_mailbox |\n\n## Data sources\n\nEvery lookup's `data.sources` array names the dataset and version behind the answer (`rfc-5322-syntax`, `live-dns`, `disposable-domains`, `role-addresses`, `free-provider-domains`, `typo-domains`, `provider-username-rules`, `mx-provider-patterns`, `rdap-registration`, the mailbox probe's provider and date, `delivery-feedback`, `suppression-list`, or `test-addresses`), so an answer can always be audited.\n",
        "termsOfService": "https://spaw.co/terms",
        "contact": {
            "name": "Spaw support",
            "email": "support@spaw.co",
            "url": "https://spaw.co"
        },
        "x-logo": {
            "url": "https://spaw.co/icons/icon-512.png",
            "altText": "Spaw"
        }
    },
    "servers": [
        {
            "url": "https://spaw.co",
            "description": "Production"
        }
    ],
    "security": [
        {
            "bearerAuth": []
        }
    ],
    "tags": [
        {
            "name": "Email",
            "description": "Single-address verification, the synchronous batch endpoint, and the browser endpoint behind publishable keys."
        },
        {
            "name": "Domain",
            "description": "Domain-level signals answered once for a whole domain."
        },
        {
            "name": "Bulk",
            "description": "Queued runs of up to 100,000 addresses with a signed completion webhook and a CSV download."
        },
        {
            "name": "Suppressions",
            "description": "The account's known-bad list, fed automatically by undeliverable verdicts and by hand."
        },
        {
            "name": "Feedback",
            "description": "Delivery outcomes you observed, which keep the suppression list current and turn verdicts into measured accuracy."
        },
        {
            "name": "Account",
            "description": "The account behind the key."
        },
        {
            "name": "Phone",
            "description": "Phone number validation and enrichment (product page marked coming soon; the endpoint works today)."
        },
        {
            "name": "IP",
            "description": "IP geolocation and risk flags (product page marked coming soon; the endpoint works today)."
        }
    ],
    "paths": {
        "/api/v1/email": {
            "post": {
                "operationId": "verify-email",
                "tags": [
                    "Email"
                ],
                "summary": "Verify an email address",
                "description": "Runs the full pipeline on one address and answers with a verdict, a machine-readable reason, an auditable 0–100 risk score, and 27 named fields. The checks run in order: address extraction (display names, `mailto:` prefixes, spreadsheet quotes and invisible characters are stripped), RFC 5322 syntax, live MX resolution including the null-MX and implicit-MX rules, the disposable, role, free-provider and typo-squat lists, provider-specific username rules, SPF and DMARC records, the domain's registration age from RDAP, and finally a mailbox-level SMTP handshake that ends before any message is transmitted.\n\nThe mailbox probe runs only when the domain accepts mail and no free signal already settled the answer, so a disposable domain or a typo-squat never reaches the mail server. Confirmed mailbox answers are shared across customers for up to a week, which is why `smtp_checked_at` can predate the lookup. When mail servers refuse to say whether an inbox exists (greylisting, timeouts), the answer degrades honestly to `smtp_checked: false` and `mailbox_exists: null`, plus a recomputable `mailbox_confidence` estimate.\n\nA single lookup never consults your suppression list: an explicit re-verify is the only way an address earns its way off it. The batch, bulk and monitor paths do honor it.\n\n### Settling an unverified answer later\n\nPass `callback_url` and `callback_secret` and an unverified answer is re-checked after 5 and 20 minutes. The settled verdict is POSTed to the callback as `{ \"event\": \"email.settled\", \"request_id\", \"settled\", \"attempt\", \"data\", \"meta\" }`, signed with the HMAC-SHA256 of the exact raw body in the `X-Spaw-Signature` header. The response to this request then carries `meta.settling: true`. Re-checks are repeats of a charged lookup, so they cost nothing; `settled` is false when the last re-check still could not say.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "1 credit for a fresh deliverable or risky verdict; undeliverable verdicts, invalid input, 7-day repeats and test addresses are free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "VALIDATION_FAILED",
                    "INSUFFICIENT_CREDITS",
                    "RATE_LIMITED"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "email"
                                ],
                                "properties": {
                                    "email": {
                                        "type": "string",
                                        "maxLength": 254,
                                        "description": "The address to verify. Wrappers people paste (a display name, a `mailto:` prefix, quotes, trailing punctuation) are stripped before checking."
                                    },
                                    "callback_url": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "uri",
                                        "maxLength": 2048,
                                        "description": "An https URL. When the answer comes back unverified (greylisted, timed out, provider unavailable), the address is re-checked after 5 and 20 minutes and the settled verdict is POSTed here."
                                    },
                                    "callback_secret": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "minLength": 16,
                                        "maxLength": 128,
                                        "description": "Required with `callback_url`. Signs the callback body — `X-Spaw-Signature` is the HMAC-SHA256 of the exact raw body, like the bulk webhook."
                                    }
                                }
                            },
                            "example": {
                                "email": "mia@acme.com"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The verdict and every field, whether or not the address is deliverable.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "$ref": "#/components/schemas/EmailResult"
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/EmailMeta"
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "email": "mia@acme.com",
                                        "normalized_email": "mia@acme.com",
                                        "is_alias": false,
                                        "is_gibberish": false,
                                        "deliverable": "deliverable",
                                        "reason": null,
                                        "risk_score": 0,
                                        "risk_level": "low",
                                        "syntax_valid": true,
                                        "domain": "acme.com",
                                        "mx_found": true,
                                        "mx_implicit": false,
                                        "mx_provider": "google",
                                        "has_spf": true,
                                        "dmarc_policy": "reject",
                                        "domain_registered_at": "1998-03-12",
                                        "domain_age_days": 10402,
                                        "disposable": false,
                                        "role": false,
                                        "free_provider": false,
                                        "smtp_checked": true,
                                        "smtp_checked_at": "2026-09-03T10:12:44+00:00",
                                        "mailbox_exists": true,
                                        "catch_all": false,
                                        "smtp_reason": null,
                                        "mailbox_confidence": null,
                                        "did_you_mean": null,
                                        "sources": [
                                            {
                                                "dataset": "rfc-5322-syntax",
                                                "version": "2026-09-03"
                                            },
                                            {
                                                "dataset": "live-dns",
                                                "version": "2026-09-03"
                                            },
                                            {
                                                "dataset": "disposable-domains",
                                                "version": "2026-09-01"
                                            },
                                            {
                                                "dataset": "mx-provider-patterns",
                                                "version": "2026-08-30"
                                            },
                                            {
                                                "dataset": "mailbox-smtp",
                                                "version": "2026-09-03"
                                            }
                                        ]
                                    },
                                    "meta": {
                                        "credits_used": 1,
                                        "credits_remaining": 9,
                                        "cache_hit": false,
                                        "suppressed": false,
                                        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "402": {
                        "$ref": "#/components/responses/InsufficientCredits"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationFailed"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/email/batch": {
            "post": {
                "operationId": "verify-email-batch",
                "tags": [
                    "Email"
                ],
                "summary": "Verify up to 50 addresses in one call",
                "description": "Verifies a list synchronously under the exact single-lookup billing rules. Every address is prepared first, then all the mailbox probes that are still needed are sent to the verification provider together, so a full batch takes about as long as a handful of single lookups. `data.results[]` mirrors the single-address response per item, in input order, and each item's `meta` carries `credits_used`, `cache_hit`, and `suppressed`.\n\nAddresses on your suppression list are served from the stored verdict at no cost and marked `suppressed: true`. Repeats inside the list are verified once. The list is walked in order and stops where the balance would run out: running out of credits mid-batch returns the paid partial results with `meta.stopped_reason: \"insufficient_credits\"` and `processed` lower than `requested`, never discarding billed work. Only a batch whose first lookup is refused answers the typed `402`.\n\nA batch counts as one request against the rate limit.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "Each address bills like a single lookup — 1 credit for a fresh deliverable or risky verdict, everything else free — and the call stops cleanly where the balance ends.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "VALIDATION_FAILED",
                    "INSUFFICIENT_CREDITS",
                    "RATE_LIMITED"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "emails"
                                ],
                                "properties": {
                                    "emails": {
                                        "type": "array",
                                        "minItems": 1,
                                        "maxItems": 50,
                                        "items": {
                                            "type": "string",
                                            "maxLength": 254
                                        },
                                        "description": "1 to 50 addresses; each item at most 254 characters."
                                    }
                                }
                            },
                            "example": {
                                "emails": [
                                    "mia@acme.com",
                                    "info@example.org",
                                    "deliverable@spaw.test"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "One result per processed address, plus the batch totals.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/BatchResponse"
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "results": [
                                            {
                                                "data": {
                                                    "email": "mia@acme.com",
                                                    "deliverable": "deliverable",
                                                    "reason": null,
                                                    "risk_score": 0,
                                                    "risk_level": "low"
                                                },
                                                "meta": {
                                                    "credits_used": 1,
                                                    "cache_hit": false,
                                                    "suppressed": false
                                                }
                                            },
                                            {
                                                "data": {
                                                    "email": "info@example.org",
                                                    "deliverable": "risky",
                                                    "reason": "role",
                                                    "risk_score": 30,
                                                    "risk_level": "medium"
                                                },
                                                "meta": {
                                                    "credits_used": 1,
                                                    "cache_hit": false,
                                                    "suppressed": false
                                                }
                                            },
                                            {
                                                "data": {
                                                    "email": "deliverable@spaw.test",
                                                    "deliverable": "deliverable",
                                                    "reason": null,
                                                    "risk_score": 0,
                                                    "risk_level": "low"
                                                },
                                                "meta": {
                                                    "credits_used": 0,
                                                    "cache_hit": false,
                                                    "suppressed": false
                                                }
                                            }
                                        ]
                                    },
                                    "meta": {
                                        "requested": 3,
                                        "processed": 3,
                                        "credits_used": 2,
                                        "credits_remaining": 7,
                                        "stopped_reason": null,
                                        "request_id": "req_01m1kgdrtqdvwnks99vfgx2rcw"
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "402": {
                        "$ref": "#/components/responses/InsufficientCredits"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationFailed"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/email/public": {
            "post": {
                "operationId": "verify-email-public",
                "tags": [
                    "Email"
                ],
                "summary": "Verify an address from the browser with a publishable key",
                "description": "The endpoint behind the form widget (`https://spaw.co/spaw-form.js`). It is authenticated by a publishable `pk_` key in the body plus the browser's `Origin` header, which must match one of the domains the key is locked to. A missing `Origin` is rejected on purpose: servers use a secret key and `POST /api/v1/email` instead.\n\nThe lookup bills the key's owner under the normal rules and answers `{ \"success\", \"data\" }` with **no meta block**, so page visitors never see the owner's balance. Because the key sits in page source, give it a daily credit cap in the dashboard: once the cap is spent the endpoint answers `429 KEY_SPEND_CAP_REACHED` until the next day. Free answers (undeliverable, cache hits, test addresses) never count toward the cap.\n\nFor forms open to the public, store a Cloudflare Turnstile site key and secret on the publishable key. The endpoint then requires a confirmed `turnstile_token` with every lookup and answers `403 TURNSTILE_FAILED` without one, before any credit is spent. Tokens are single-use. The form widget obtains a fresh token per lookup when the script tag carries `data-turnstile-site-key`.\n\nThrottled at 20 requests per minute per IP.\n",
                "security": [],
                "x-spaw-auth": "publishable-key",
                "x-spaw-billing": "Bills the key owner like a single lookup; capped by the key's daily credit cap when one is set.",
                "x-spaw-errors": [
                    "INVALID_PUBLISHABLE_KEY",
                    "ORIGIN_NOT_ALLOWED",
                    "TURNSTILE_FAILED",
                    "KEY_SPEND_CAP_REACHED",
                    "VALIDATION_FAILED",
                    "INSUFFICIENT_CREDITS",
                    "RATE_LIMITED"
                ],
                "parameters": [
                    {
                        "name": "Origin",
                        "in": "header",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "example": "https://www.example.com",
                        "description": "Sent by browsers automatically. Its host must be on the key's allowed-domain list."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "key",
                                    "email"
                                ],
                                "properties": {
                                    "key": {
                                        "type": "string",
                                        "maxLength": 64,
                                        "description": "A publishable key, which starts with `pk_`."
                                    },
                                    "email": {
                                        "type": "string",
                                        "maxLength": 254,
                                        "description": "The address to verify."
                                    },
                                    "turnstile_token": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 2048,
                                        "description": "Required when the key carries a Turnstile pair. A single-use token from the Turnstile widget on the page."
                                    }
                                }
                            },
                            "example": {
                                "key": "pk_live_…",
                                "email": "mia@acme.com"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The same fields as `POST /api/v1/email`, without a meta block.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "$ref": "#/components/schemas/EmailResult"
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "email": "mia@acme.com",
                                        "deliverable": "deliverable",
                                        "reason": null,
                                        "risk_score": 0,
                                        "risk_level": "low",
                                        "did_you_mean": null
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "The publishable key does not exist or was revoked.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorEnvelope"
                                },
                                "example": {
                                    "success": false,
                                    "error": {
                                        "code": "INVALID_PUBLISHABLE_KEY",
                                        "message": "That publishable key does not exist or was revoked.",
                                        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                                    }
                                }
                            }
                        }
                    },
                    "402": {
                        "$ref": "#/components/responses/InsufficientCredits"
                    },
                    "403": {
                        "description": "The page's origin is not on the key's allowed-domain list (`ORIGIN_NOT_ALLOWED`), or the key requires a Turnstile token that was missing or not confirmed (`TURNSTILE_FAILED`).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorEnvelope"
                                },
                                "example": {
                                    "success": false,
                                    "error": {
                                        "code": "ORIGIN_NOT_ALLOWED",
                                        "message": "This publishable key cannot be used from this origin.",
                                        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationFailed"
                    },
                    "429": {
                        "description": "The key has spent its owner's daily credit cap (`KEY_SPEND_CAP_REACHED`), or the IP exceeded 20 requests per minute (`RATE_LIMITED`).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorEnvelope"
                                },
                                "example": {
                                    "success": false,
                                    "error": {
                                        "code": "KEY_SPEND_CAP_REACHED",
                                        "message": "This publishable key has reached its daily credit cap. The counter resets each day.",
                                        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/v1/email/domain/{domain}": {
            "get": {
                "operationId": "domain-intelligence",
                "tags": [
                    "Domain"
                ],
                "summary": "Inspect a domain's mail setup",
                "description": "The domain-level signals of an email lookup, answered once for a whole domain: whether it can receive mail (MX records, or the RFC 5321 implicit fallback), who runs its mail, whether it publishes SPF and which DMARC policy, when it was registered, whether it is on the disposable or free-provider lists, and whether any address there has answered catch-all within the last week.\n\nInvalid input is an answer, not a validation error: a string that is not a hostname answers `200` with `valid: false` and every other field null. A domain with no usable mail server is free. The per-account 7-day repeat cache applies.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "1 credit only when the domain accepts mail (`mx_found` true) and the lookup is fresh; invalid or dead domains and 7-day repeats are free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "INSUFFICIENT_CREDITS",
                    "RATE_LIMITED"
                ],
                "parameters": [
                    {
                        "name": "domain",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "maxLength": 254
                        },
                        "example": "acme.com",
                        "description": "The domain to inspect. Lowercased and converted to punycode before checking."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The domain's signals; `valid: false` for input that is not a hostname.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "$ref": "#/components/schemas/DomainIntelligence"
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/Meta"
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "domain": "acme.com",
                                        "valid": true,
                                        "mx_found": true,
                                        "mx_implicit": false,
                                        "catch_all": null,
                                        "mx_provider": "google",
                                        "has_spf": true,
                                        "dmarc_policy": "reject",
                                        "domain_registered_at": "1998-03-12",
                                        "domain_age_days": 10402,
                                        "disposable": false,
                                        "free_provider": false,
                                        "sources": [
                                            {
                                                "dataset": "live-dns",
                                                "version": "2026-09-03"
                                            },
                                            {
                                                "dataset": "disposable-domains",
                                                "version": "2026-09-01"
                                            },
                                            {
                                                "dataset": "free-provider-domains",
                                                "version": "2026-09-01"
                                            },
                                            {
                                                "dataset": "mx-provider-patterns",
                                                "version": "2026-08-30"
                                            },
                                            {
                                                "dataset": "rdap-registration",
                                                "version": "2026-09-03"
                                            }
                                        ]
                                    },
                                    "meta": {
                                        "credits_used": 1,
                                        "credits_remaining": 8,
                                        "cache_hit": false,
                                        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "402": {
                        "$ref": "#/components/responses/InsufficientCredits"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/email/bulk": {
            "post": {
                "operationId": "create-bulk-job",
                "tags": [
                    "Bulk"
                ],
                "summary": "Queue a bulk verification run",
                "description": "Queues up to 100,000 addresses and processes them in the background in chunks, with every chunk's mailbox probes sent concurrently. Creation answers `202` with the job's id and status. Poll `GET /api/v1/email/bulk/{jobId}` for progress, then download the result CSV from `GET /api/v1/email/bulk/{jobId}/results`. Jobs and their files are kept for 30 days.\n\nRows repeating an address already in the list are verified once and counted in `duplicate_count`. Addresses at a domain already known to be catch-all, addresses on your suppression list, and addresses your account reported delivered recently answer without a probe. A run that exhausts the balance stops with `stopped_reason: \"insufficient_credits\"` and keeps everything verified so far.\n\n### Completion webhook\n\nWith a `webhook_url`, the finish (`completed`, `failed`, or `cancelled`) is POSTed there as `{ \"event\": \"bulk_email_job.finished\", \"job\": { \"id\", \"status\", \"total\", \"processed\", \"deliverable\", \"risky\", \"undeliverable\", \"credits_used\", \"stopped_reason\", \"finished_at\" } }`. The `X-Spaw-Signature` header is the HMAC-SHA256 of the exact raw body keyed with `webhook_secret`, which this response returns once and never again. Connection errors and 5xx answers are retried three times with a short backoff.\n\n```php\n$expected = hash_hmac('sha256', $request->getContent(), $secret);\nabort_unless(hash_equals($expected, $request->header('X-Spaw-Signature')), 401);\n```\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "Each row bills like a single lookup as it is processed; repeats inside the run and 7-day repeats are free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "VALIDATION_FAILED",
                    "RATE_LIMITED"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "emails"
                                ],
                                "properties": {
                                    "emails": {
                                        "type": "array",
                                        "minItems": 1,
                                        "maxItems": 100000,
                                        "items": {
                                            "type": "string",
                                            "maxLength": 254
                                        },
                                        "description": "1 to 100,000 addresses; each item at most 254 characters."
                                    },
                                    "webhook_url": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "uri",
                                        "maxLength": 2048,
                                        "description": "An https URL to POST when the job finishes. The response includes the signing secret — shown once, never again."
                                    }
                                }
                            },
                            "example": {
                                "emails": [
                                    "mia@acme.com",
                                    "info@example.org"
                                ],
                                "webhook_url": "https://www.example.com/hooks/spaw"
                            }
                        }
                    }
                },
                "responses": {
                    "202": {
                        "description": "The job was queued.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "type": "object",
                                            "properties": {
                                                "job": {
                                                    "allOf": [
                                                        {
                                                            "$ref": "#/components/schemas/BulkJob"
                                                        },
                                                        {
                                                            "type": "object",
                                                            "properties": {
                                                                "webhook_secret": {
                                                                    "type": [
                                                                        "string",
                                                                        "null"
                                                                    ],
                                                                    "description": "Signs the completion webhook. Returned only here."
                                                                }
                                                            }
                                                        }
                                                    ]
                                                }
                                            }
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "job": {
                                            "id": 512,
                                            "status": "queued",
                                            "total": 2,
                                            "duplicate_count": 0,
                                            "processed": 0,
                                            "deliverable": 0,
                                            "risky": 0,
                                            "undeliverable": 0,
                                            "credits_used": 0,
                                            "stopped_reason": null,
                                            "cancel_requested": false,
                                            "webhook_status": null,
                                            "created_at": "2026-09-03T10:12:44+00:00",
                                            "finished_at": null,
                                            "webhook_secret": "8fJ2…40 characters…Qk1"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationFailed"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/email/bulk/{jobId}": {
            "get": {
                "operationId": "get-bulk-job",
                "tags": [
                    "Bulk"
                ],
                "summary": "Poll a bulk job",
                "description": "The job's current status and running counts. `status` moves from `queued` to `processing` and ends in `completed`, `failed`, or `cancelled`; `processed` and the verdict counts advance as chunks finish. A job that belongs to another account answers `404`.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "Free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "RATE_LIMITED"
                ],
                "parameters": [
                    {
                        "name": "jobId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "example": 512,
                        "description": "The id returned at creation."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The job.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "type": "object",
                                            "properties": {
                                                "job": {
                                                    "$ref": "#/components/schemas/BulkJob"
                                                }
                                            }
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "job": {
                                            "id": 512,
                                            "status": "processing",
                                            "total": 2000,
                                            "duplicate_count": 12,
                                            "processed": 900,
                                            "deliverable": 610,
                                            "risky": 140,
                                            "undeliverable": 150,
                                            "credits_used": 750,
                                            "stopped_reason": null,
                                            "cancel_requested": false,
                                            "webhook_status": null,
                                            "created_at": "2026-09-03T10:12:44+00:00",
                                            "finished_at": null
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/email/bulk/{jobId}/cancel": {
            "post": {
                "operationId": "cancel-bulk-job",
                "tags": [
                    "Bulk"
                ],
                "summary": "Cancel a bulk job",
                "description": "Asks a queued or running job to stop. A queued job is cancelled on the spot; a running one stops at its next checkpoint and keeps the rows already written downloadable through the results endpoint. Rows already verified stay billed. The completion webhook fires with status `cancelled` just as it does for `completed` and `failed`. A job that already reached a terminal status answers `409 JOB_ALREADY_FINISHED`.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "Free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "JOB_ALREADY_FINISHED",
                    "RATE_LIMITED"
                ],
                "parameters": [
                    {
                        "name": "jobId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "example": 512,
                        "description": "The id returned at creation."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The job, with `cancel_requested` set.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "type": "object",
                                            "properties": {
                                                "job": {
                                                    "$ref": "#/components/schemas/BulkJob"
                                                }
                                            }
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "job": {
                                            "id": 512,
                                            "status": "processing",
                                            "total": 2000,
                                            "duplicate_count": 12,
                                            "processed": 900,
                                            "deliverable": 610,
                                            "risky": 140,
                                            "undeliverable": 150,
                                            "credits_used": 750,
                                            "stopped_reason": null,
                                            "cancel_requested": true,
                                            "webhook_status": null,
                                            "created_at": "2026-09-03T10:12:44+00:00",
                                            "finished_at": null
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "409": {
                        "description": "The job already finished.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorEnvelope"
                                },
                                "example": {
                                    "success": false,
                                    "error": {
                                        "code": "JOB_ALREADY_FINISHED",
                                        "message": "The job has already finished and cannot be cancelled.",
                                        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                                    }
                                }
                            }
                        }
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/email/bulk/{jobId}/results": {
            "get": {
                "operationId": "download-bulk-results",
                "tags": [
                    "Bulk"
                ],
                "summary": "Download a bulk job's results as CSV",
                "description": "Streams the run's result file: a finished job's full results, or the partial file a mid-run cancellation kept. The file starts with the input's own columns (for an API list, a single `email` column; for a dashboard upload, every column of the uploaded file), followed by `verified_email` and the verdict fields, one row per input row in the original order:\n\n`verified_email, deliverable, reason, risk_score, risk_level, normalized_email, is_alias, is_gibberish, disposable, role, free_provider, mx_found, mx_implicit, mx_provider, has_spf, dmarc_policy, domain_registered_at, domain_age_days, smtp_checked, smtp_checked_at, mailbox_exists, catch_all, smtp_reason, mailbox_confidence, did_you_mean, credits_used, cache_hit, suppressed`\n\nAdd `?variant=deliverable` (or `risky`, `undeliverable`) to download only those rows. A job still running answers `409 RESULTS_NOT_READY`; a job cancelled before any address was processed answers `409 JOB_CANCELLED`.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "Free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "RESULTS_NOT_READY",
                    "JOB_CANCELLED",
                    "VALIDATION_FAILED",
                    "RATE_LIMITED"
                ],
                "parameters": [
                    {
                        "name": "jobId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "example": 512,
                        "description": "The id returned at creation."
                    },
                    {
                        "name": "variant",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "string",
                            "enum": [
                                "full",
                                "deliverable",
                                "risky",
                                "undeliverable"
                            ],
                            "default": "full"
                        },
                        "example": "deliverable",
                        "description": "Which rows to include."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The CSV, as an attachment named `spaw-bulk-{jobId}.csv` (with `-{variant}` appended for a filtered download).",
                        "content": {
                            "text/csv": {
                                "schema": {
                                    "type": "string"
                                },
                                "example": "email,verified_email,deliverable,reason,risk_score,risk_level,normalized_email,is_alias,is_gibberish,disposable,role,free_provider,mx_found,mx_implicit,mx_provider,has_spf,dmarc_policy,domain_registered_at,domain_age_days,smtp_checked,smtp_checked_at,mailbox_exists,catch_all,smtp_reason,mailbox_confidence,did_you_mean,credits_used,cache_hit,suppressed\nmia@acme.com,mia@acme.com,deliverable,,0,low,mia@acme.com,0,0,0,0,0,1,0,google,1,reject,1998-03-12,10402,1,2026-09-03T10:12:44+00:00,1,0,,,,1,0,0\n"
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "409": {
                        "description": "The results are not available yet (`RESULTS_NOT_READY`) or never will be (`JOB_CANCELLED`).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorEnvelope"
                                },
                                "example": {
                                    "success": false,
                                    "error": {
                                        "code": "RESULTS_NOT_READY",
                                        "message": "The job has not finished yet. Poll its status until it reports completed.",
                                        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "The `variant` query parameter is not one of the allowed values.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorEnvelope"
                                },
                                "example": {
                                    "success": false,
                                    "error": {
                                        "code": "VALIDATION_FAILED",
                                        "message": "variant must be one of: full, deliverable, risky, undeliverable.",
                                        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                                    }
                                }
                            }
                        }
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/email/suppressions": {
            "get": {
                "operationId": "list-suppressions",
                "tags": [
                    "Suppressions"
                ],
                "summary": "List the suppression list",
                "description": "The account's suppression list, newest first, 100 entries per page. Every undeliverable verdict adds the address in normalized form (lowercased, plus-tag stripped, Gmail dots folded) with source `auto`; imports arrive with source `imported`; bounces and complaints reported through the feedback endpoints arrive with source `feedback`. Batch, bulk and monitor runs answer suppressed addresses from the stored verdict at no cost; a single lookup always re-verifies, and any verdict other than undeliverable removes the entry. Entries untouched for 90 days are re-verified instead of trusted.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "Free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "RATE_LIMITED"
                ],
                "parameters": [
                    {
                        "name": "page",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "default": 1
                        },
                        "example": 1,
                        "description": "The page to return."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "One page of entries.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "type": "object",
                                            "properties": {
                                                "suppressions": {
                                                    "type": "array",
                                                    "items": {
                                                        "$ref": "#/components/schemas/Suppression"
                                                    }
                                                }
                                            }
                                        },
                                        "meta": {
                                            "type": "object",
                                            "properties": {
                                                "page": {
                                                    "type": "integer"
                                                },
                                                "per_page": {
                                                    "type": "integer"
                                                },
                                                "total": {
                                                    "type": "integer"
                                                }
                                            }
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "suppressions": [
                                            {
                                                "id": 9041,
                                                "email": "old@acme.com",
                                                "reason": "mailbox_not_found",
                                                "source": "auto",
                                                "added_at": "2026-09-02T08:30:00+00:00"
                                            },
                                            {
                                                "id": 9040,
                                                "email": "bounced@example.org",
                                                "reason": null,
                                                "source": "imported",
                                                "added_at": "2026-09-01T17:02:11+00:00"
                                            }
                                        ]
                                    },
                                    "meta": {
                                        "page": 1,
                                        "per_page": 100,
                                        "total": 2
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            },
            "post": {
                "operationId": "add-suppressions",
                "tags": [
                    "Suppressions"
                ],
                "summary": "Import known-bad addresses",
                "description": "Adds up to 10,000 addresses per request to the suppression list — an email provider's bounce export, a hand-kept blocklist. Addresses are normalized first; ones that do not parse are counted as `invalid`. Entries that already exist are never overwritten, because a verified auto-suppression outranks an import, and are counted as `already_suppressed`. Imported entries are honored from day one at no credit cost: list, bulk and monitor runs answer them with reason `suppressed` and `risk_score` 100.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "Free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "VALIDATION_FAILED",
                    "RATE_LIMITED"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "emails"
                                ],
                                "properties": {
                                    "emails": {
                                        "type": "array",
                                        "minItems": 1,
                                        "maxItems": 10000,
                                        "items": {
                                            "type": "string",
                                            "maxLength": 254
                                        },
                                        "description": "1 to 10,000 addresses; each item at most 254 characters."
                                    }
                                }
                            },
                            "example": {
                                "emails": [
                                    "bounced@example.org",
                                    "gone@acme.com"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "How many entries were added, already present, or unparseable.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "type": "object",
                                            "properties": {
                                                "added": {
                                                    "type": "integer"
                                                },
                                                "already_suppressed": {
                                                    "type": "integer"
                                                },
                                                "invalid": {
                                                    "type": "integer"
                                                }
                                            }
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "added": 2,
                                        "already_suppressed": 0,
                                        "invalid": 0
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationFailed"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/email/suppressions/{suppressionId}": {
            "delete": {
                "operationId": "remove-suppression",
                "tags": [
                    "Suppressions"
                ],
                "summary": "Remove a suppression entry",
                "description": "Deletes one entry so the address is verified fresh the next time a list, bulk or monitor run meets it. An entry that belongs to another account answers `404`.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "Free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "RATE_LIMITED"
                ],
                "parameters": [
                    {
                        "name": "suppressionId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "example": 9041,
                        "description": "The entry's id from the list endpoint."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The entry was removed.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        }
                                    }
                                },
                                "example": {
                                    "success": true
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/email/feedback": {
            "post": {
                "operationId": "report-delivery-feedback",
                "tags": [
                    "Feedback"
                ],
                "summary": "Report delivery outcomes",
                "description": "Tells Spaw what actually happened to an address after you sent to it: `delivered`, `bounced` (hard bounces only), or `complained`. Each outcome is stored against the verdict you had been given for that address in the previous 90 days, which turns verdicts into measured accuracy, and it keeps your suppression list current: a bounce or complaint adds the address with source `feedback`, a delivery removes an entry Spaw created automatically. An address you reported delivered within the last 90 days also counts as a confirmed mailbox for your own later lookups (`smtp_reason: \"delivered_recently\"`), without a probe.\n\nFeedback is free and never logged as a lookup. Items whose address does not parse are skipped and counted, not rejected. Outcomes are kept for 180 days.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "Free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "VALIDATION_FAILED",
                    "RATE_LIMITED"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "items"
                                ],
                                "properties": {
                                    "items": {
                                        "type": "array",
                                        "minItems": 1,
                                        "maxItems": 1000,
                                        "items": {
                                            "$ref": "#/components/schemas/FeedbackItem"
                                        },
                                        "description": "1 to 1,000 outcomes per request."
                                    }
                                }
                            },
                            "example": {
                                "items": [
                                    {
                                        "email": "mia@acme.com",
                                        "outcome": "bounced",
                                        "reason": "550 5.1.1 no such user"
                                    },
                                    {
                                        "email": "sam@example.org",
                                        "outcome": "delivered",
                                        "occurred_at": "2026-09-03T09:00:00Z"
                                    }
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "202": {
                        "description": "The outcomes were recorded.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "type": "object",
                                            "properties": {
                                                "recorded": {
                                                    "type": "integer"
                                                },
                                                "skipped": {
                                                    "type": "integer",
                                                    "description": "Items whose address did not parse."
                                                }
                                            }
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "recorded": 2,
                                        "skipped": 0
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationFailed"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/email/feedback/summary": {
            "get": {
                "operationId": "feedback-summary",
                "tags": [
                    "Feedback"
                ],
                "summary": "Measured accuracy",
                "description": "Crosses the last 90 days of reported outcomes with the verdicts on file. `by_verdict` counts delivered, bounced and complained per verdict (`deliverable`, `risky`, `undeliverable`, and `unknown` for addresses with no verdict on file), and `measured.deliverable_delivery_rate` and `measured.undeliverable_bounce_rate` are the two numbers that say how right Spaw was for you. A rate is `null` until there is at least one matching outcome. The same figures appear on the History tab of the dashboard.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "Free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "RATE_LIMITED"
                ],
                "responses": {
                    "200": {
                        "description": "The summary.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "$ref": "#/components/schemas/FeedbackSummary"
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "window_days": 90,
                                        "total": 1240,
                                        "outcomes": {
                                            "delivered": 1180,
                                            "bounced": 52,
                                            "complained": 8
                                        },
                                        "by_verdict": {
                                            "deliverable": {
                                                "delivered": 1150,
                                                "bounced": 9,
                                                "complained": 6
                                            },
                                            "risky": {
                                                "delivered": 30,
                                                "bounced": 11,
                                                "complained": 2
                                            },
                                            "undeliverable": {
                                                "delivered": 0,
                                                "bounced": 32,
                                                "complained": 0
                                            },
                                            "unknown": {
                                                "delivered": 0,
                                                "bounced": 0,
                                                "complained": 0
                                            }
                                        },
                                        "measured": {
                                            "deliverable_delivery_rate": 0.992,
                                            "undeliverable_bounce_rate": 1
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/email/feedback/{provider}/{feedbackKey}": {
            "post": {
                "operationId": "esp-bounce-webhook",
                "tags": [
                    "Feedback"
                ],
                "summary": "Receive bounce webhooks from an email provider",
                "description": "The webhook target for an email provider's bounce, delivery and complaint notifications. Providers cannot send bearer tokens, so the account's feedback key rides in the URL (generate or rotate it on the dashboard's API keys page; rotating invalidates the URLs at once) and the provider name selects the payload adapter. The body is the provider's own payload, decoded as JSON regardless of content type because Amazon SNS posts JSON as `text/plain`. Payloads are read for their documented fields and nothing else; events that say nothing definitive about the address are ignored.\n\n| Provider | Path | Mapping |\n| --- | --- | --- |\n| postmark | `/api/v1/email/feedback/postmark/{key}` | Bounce (hard types) → bounced · Delivery → delivered · SpamComplaint → complained. Soft bounces are ignored. |\n| ses | `/api/v1/email/feedback/ses/{key}` | SNS notifications: Bounce with bounceType Permanent → bounced · Delivery → delivered · Complaint → complained. Subscription confirmations are answered. |\n| mailgun | `/api/v1/email/feedback/mailgun/{key}` | delivered → delivered · failed with severity permanent → bounced · complained → complained. Temporary failures are ignored. |\n| sendgrid | `/api/v1/email/feedback/sendgrid/{key}` | delivered → delivered · bounce and dropped → bounced · spamreport → complained. Deferred events are ignored. |\n\nThrottled at 120 requests per minute per IP. Outcomes recorded here behave exactly like ones reported through `POST /api/v1/email/feedback`.\n",
                "security": [],
                "x-spaw-auth": "feedback-key",
                "x-spaw-billing": "Free.",
                "x-spaw-errors": [
                    "UNKNOWN_FEEDBACK_PROVIDER",
                    "INVALID_FEEDBACK_KEY",
                    "RATE_LIMITED"
                ],
                "parameters": [
                    {
                        "name": "provider",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "enum": [
                                "postmark",
                                "ses",
                                "mailgun",
                                "sendgrid"
                            ]
                        },
                        "example": "postmark",
                        "description": "Which provider's payload format to expect."
                    },
                    {
                        "name": "feedbackKey",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "example": "fb_…",
                        "description": "The account's feedback key, which starts with `fb_`."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "description": "The provider's native webhook payload.",
                                "additionalProperties": true
                            },
                            "example": {
                                "RecordType": "Bounce",
                                "Type": "HardBounce",
                                "Email": "mia@acme.com",
                                "Description": "The server was unable to deliver your message (ex. unknown user, mailbox not found).",
                                "BouncedAt": "2026-09-03T10:12:44Z"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The outcomes the payload carried were recorded.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "type": "object",
                                            "properties": {
                                                "recorded": {
                                                    "type": "integer"
                                                },
                                                "skipped": {
                                                    "type": "integer"
                                                }
                                            }
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "recorded": 1,
                                        "skipped": 0
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "The feedback key does not exist or was rotated.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorEnvelope"
                                },
                                "example": {
                                    "success": false,
                                    "error": {
                                        "code": "INVALID_FEEDBACK_KEY",
                                        "message": "That feedback key does not exist or was rotated.",
                                        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                                    }
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "The provider path is not one of postmark, ses, mailgun, or sendgrid.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorEnvelope"
                                },
                                "example": {
                                    "success": false,
                                    "error": {
                                        "code": "UNKNOWN_FEEDBACK_PROVIDER",
                                        "message": "Supported providers: postmark, ses, mailgun, sendgrid.",
                                        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                                    }
                                }
                            }
                        }
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/account": {
            "get": {
                "operationId": "get-account",
                "tags": [
                    "Account"
                ],
                "summary": "Read the account and credit balance",
                "description": "The account behind the presented key: its name and email, the plan, the live credit balance, and the monthly free grant. Useful for dashboards and pre-flight checks before a big run. Reading the balance applies the month's free grant if it has not been applied yet.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "Free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "RATE_LIMITED"
                ],
                "responses": {
                    "200": {
                        "description": "The account.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "$ref": "#/components/schemas/Account"
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "name": "Mia Kowalski",
                                        "email": "mia@acme.com",
                                        "plan": "free",
                                        "credits": {
                                            "balance": 9,
                                            "monthly_grant": 10
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/phone": {
            "post": {
                "operationId": "validate-phone",
                "tags": [
                    "Phone"
                ],
                "summary": "Validate and enrich a phone number",
                "description": "Parses a number with libphonenumber metadata and answers whether it is valid for its region, its E.164, national and international formats, the country, the line type (`mobile`, `fixed_line`, `fixed_line_or_mobile`, `voip`, `toll_free`, …), the carrier and region where the metadata knows them, and the time zones the number belongs to. Pass `country` (an ISO 3166-1 alpha-2 code) to parse numbers written without a country calling code.\n\nThe phone product page is marked \"coming soon\"; the endpoint itself works today under the same envelope, billing and error codes as every other lookup.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "1 credit for a fresh lookup that answers `valid` true; invalid numbers and 7-day repeats are free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "VALIDATION_FAILED",
                    "INSUFFICIENT_CREDITS",
                    "RATE_LIMITED"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "phone"
                                ],
                                "properties": {
                                    "phone": {
                                        "type": "string",
                                        "maxLength": 32,
                                        "description": "The number, in any common notation. Include the country calling code or pass `country`."
                                    },
                                    "country": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "description": "An ISO 3166-1 alpha-2 region code (`US`, `GB`, `PL`) used to parse numbers written without a calling code."
                                    }
                                }
                            },
                            "example": {
                                "phone": "+1 415 555 0142"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The parsed number; `valid: false` with a `reason` for numbers that do not parse.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "$ref": "#/components/schemas/PhoneResult"
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/Meta"
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "valid": true,
                                        "reason": null,
                                        "e164": "+14155550142",
                                        "national": "(415) 555-0142",
                                        "international": "+1 415-555-0142",
                                        "country": "US",
                                        "line_type": "fixed_line_or_mobile",
                                        "carrier": null,
                                        "region": "San Francisco, CA",
                                        "extension": null,
                                        "timezones": [
                                            "America/Los_Angeles"
                                        ],
                                        "sources": [
                                            {
                                                "dataset": "libphonenumber",
                                                "version": "9.0.37"
                                            }
                                        ]
                                    },
                                    "meta": {
                                        "credits_used": 1,
                                        "credits_remaining": 8,
                                        "cache_hit": false,
                                        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "402": {
                        "$ref": "#/components/responses/InsufficientCredits"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationFailed"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        },
        "/api/v1/ip": {
            "post": {
                "operationId": "lookup-ip",
                "tags": [
                    "IP"
                ],
                "summary": "Locate and risk-score an IP address",
                "description": "Locates an IPv4 or IPv6 address with the DB-IP Lite city and ASN databases and flags datacenter, Tor exit and VPN ranges from open threat lists. The flags are tri-state: `false` is a checked negative, `null` means the signal was not evaluated (list not installed, or IPv6 where a list is IPv4-only). `risk_score` sums only the signals that were checked — Tor 70, datacenter 40, VPN 30, capped at 100 — and `risk_level` buckets it with the same thresholds as email: 60 and above high, 30 and above medium. Reserved and private ranges answer `reason: \"reserved_range\"` with every field null.\n\nThe IP product page is marked \"coming soon\"; the endpoint itself works today.\n",
                "x-spaw-auth": "bearer",
                "x-spaw-billing": "1 credit for a fresh lookup that resolves at least one signal (a location, a network, or a positive threat flag); reserved ranges, unknown addresses and 7-day repeats are free.",
                "x-spaw-errors": [
                    "UNAUTHENTICATED",
                    "VALIDATION_FAILED",
                    "INSUFFICIENT_CREDITS",
                    "RATE_LIMITED"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "ip"
                                ],
                                "properties": {
                                    "ip": {
                                        "type": "string",
                                        "description": "An IPv4 or IPv6 address."
                                    }
                                }
                            },
                            "example": {
                                "ip": "8.8.8.8"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The location, network and threat flags.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "data": {
                                            "$ref": "#/components/schemas/IpResult"
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/Meta"
                                        }
                                    }
                                },
                                "example": {
                                    "success": true,
                                    "data": {
                                        "ip": "8.8.8.8",
                                        "version": 4,
                                        "reason": null,
                                        "country": "US",
                                        "region": "California",
                                        "city": "Mountain View",
                                        "latitude": 37.4056,
                                        "longitude": -122.0775,
                                        "asn": 15169,
                                        "org": "Google LLC",
                                        "is_datacenter": true,
                                        "is_tor": false,
                                        "is_vpn": false,
                                        "risk_score": 40,
                                        "risk_level": "medium",
                                        "sources": [
                                            {
                                                "dataset": "dbip-city-lite",
                                                "version": "2026-09"
                                            },
                                            {
                                                "dataset": "dbip-asn-lite",
                                                "version": "2026-09"
                                            },
                                            {
                                                "dataset": "tor-exit-list",
                                                "version": "2026-09-03"
                                            },
                                            {
                                                "dataset": "datacenter-ranges",
                                                "version": "2026-09-03"
                                            },
                                            {
                                                "dataset": "vpn-ranges",
                                                "version": "2026-09-03"
                                            }
                                        ]
                                    },
                                    "meta": {
                                        "credits_used": 1,
                                        "credits_remaining": 8,
                                        "cache_hit": false,
                                        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthenticated"
                    },
                    "402": {
                        "$ref": "#/components/responses/InsufficientCredits"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationFailed"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "bearerAuth": {
                "type": "http",
                "scheme": "bearer",
                "bearerFormat": "sk_live_ API key",
                "description": "A secret API key from the dashboard's API keys page, sent as a bearer token."
            }
        },
        "responses": {
            "Unauthenticated": {
                "description": "The key is missing, malformed, or revoked.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorEnvelope"
                        },
                        "example": {
                            "success": false,
                            "error": {
                                "code": "UNAUTHENTICATED",
                                "message": "Provide a valid API key as a bearer token.",
                                "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                            }
                        }
                    }
                }
            },
            "ValidationFailed": {
                "description": "The request body could not be validated; `error.errors` lists the fields.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorEnvelope"
                        },
                        "example": {
                            "success": false,
                            "error": {
                                "code": "VALIDATION_FAILED",
                                "message": "The email field is required.",
                                "errors": {
                                    "email": [
                                        "The email field is required."
                                    ]
                                },
                                "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                            }
                        }
                    }
                }
            },
            "InsufficientCredits": {
                "description": "The balance is empty. The lookup did not run.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorEnvelope"
                        },
                        "example": {
                            "success": false,
                            "error": {
                                "code": "INSUFFICIENT_CREDITS",
                                "message": "Your credit balance is empty.",
                                "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                            }
                        }
                    }
                }
            },
            "RateLimited": {
                "description": "Over 5 requests per second for the key. Retry after the limit resets.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/ErrorEnvelope"
                        },
                        "example": {
                            "success": false,
                            "error": {
                                "code": "RATE_LIMITED",
                                "message": "Too many requests. Retry after the limit resets.",
                                "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
                            }
                        }
                    }
                }
            },
            "NotFound": {
                "description": "No such record on this account.",
                "content": {
                    "application/json": {
                        "schema": {
                            "type": "object",
                            "properties": {
                                "message": {
                                    "type": "string"
                                }
                            }
                        },
                        "example": {
                            "message": "Not Found"
                        }
                    }
                }
            }
        },
        "schemas": {
            "Source": {
                "type": "object",
                "description": "A dataset consulted for the answer and the version that was current.",
                "properties": {
                    "dataset": {
                        "type": "string",
                        "example": "live-dns"
                    },
                    "version": {
                        "type": "string",
                        "example": "2026-09-03"
                    }
                }
            },
            "Meta": {
                "type": "object",
                "description": "The billing block on every lookup.",
                "properties": {
                    "credits_used": {
                        "type": "integer",
                        "description": "Credits this call cost (0 or 1)."
                    },
                    "credits_remaining": {
                        "type": "integer",
                        "description": "The balance after this call."
                    },
                    "cache_hit": {
                        "type": "boolean",
                        "description": "Whether the answer was a free 7-day repeat."
                    },
                    "request_id": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Matches the X-Request-Id header and every log line of the request."
                    }
                }
            },
            "EmailMeta": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/Meta"
                    },
                    {
                        "type": "object",
                        "properties": {
                            "suppressed": {
                                "type": "boolean",
                                "description": "Whether the answer was served from the suppression list (always false on a single lookup)."
                            },
                            "settling": {
                                "type": "boolean",
                                "description": "Present only when a callback was requested — true when a re-check was scheduled, false when the answer was already final."
                            }
                        }
                    }
                ]
            },
            "ErrorEnvelope": {
                "type": "object",
                "description": "Every failure answers this shape with a stable code to branch on.",
                "properties": {
                    "success": {
                        "type": "boolean",
                        "const": false
                    },
                    "error": {
                        "type": "object",
                        "properties": {
                            "code": {
                                "type": "string",
                                "description": "A stable code such as INSUFFICIENT_CREDITS. See https://spaw.co/docs/errors."
                            },
                            "message": {
                                "type": "string"
                            },
                            "errors": {
                                "type": "object",
                                "description": "Present on VALIDATION_FAILED — the failing fields and their messages.",
                                "additionalProperties": {
                                    "type": "array",
                                    "items": {
                                        "type": "string"
                                    }
                                }
                            },
                            "request_id": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            }
                        }
                    }
                }
            },
            "EmailResult": {
                "type": "object",
                "description": "The 27 documented fields of an email verification, plus the sources consulted.",
                "properties": {
                    "email": {
                        "type": "string",
                        "description": "The address as checked — local part preserved, domain lowercased and converted to punycode. Wrappers people paste (a display name, a `mailto:` prefix, spreadsheet quotes, trailing punctuation, invisible characters) are stripped first. Input that still fails the syntax check is echoed back."
                    },
                    "normalized_email": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The canonical inbox the mail actually reaches — lowercased, plus-tag stripped, Gmail dots and googlemail.com folded away. Dedupe your list on this, not on the raw input."
                    },
                    "is_alias": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Whether the address carries a plus-tag (mia+promo@…) — the same inbox under a disposable label."
                    },
                    "is_gibberish": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Whether the local part reads as keyboard mash. Deliberately conservative — y counts as a vowel, digit-only locals are never flagged."
                    },
                    "deliverable": {
                        "type": "string",
                        "enum": [
                            "deliverable",
                            "risky",
                            "undeliverable"
                        ],
                        "description": "The verdict. Risky covers a burner, a typo-squat, a web-only domain, a role inbox, a full mailbox, or a catch-all; undeliverable covers bad syntax, an impossible username, no usable mail servers, or a missing or disabled mailbox."
                    },
                    "reason": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "enum": [
                            "invalid_syntax",
                            "invalid_local_part",
                            "no_mx_records",
                            "null_mx",
                            "mx_unresolvable",
                            "mailbox_not_found",
                            "mailbox_disabled",
                            "likely_typo",
                            "disposable",
                            "implicit_mx",
                            "mailbox_full",
                            "role",
                            "catch_all",
                            "suppressed",
                            null
                        ],
                        "description": "Why the verdict is not deliverable. Each value is explained at https://spaw.co/docs/reasons."
                    },
                    "risk_score": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 100,
                        "description": "A branchable composite with published weights — disposable 80, typo-squat 60, web-only domain 30, role 30, catch-all 30, domain under 30 days old 30, full mailbox 20, gibberish 20, domain under 180 days 10, no SPF on a business domain 10, unverified mailbox 10, plus-tag alias 5. Undeliverable is always 100."
                    },
                    "risk_level": {
                        "type": "string",
                        "enum": [
                            "low",
                            "medium",
                            "high"
                        ],
                        "description": "The score bucketed — medium from 30, high from 60."
                    },
                    "syntax_valid": {
                        "type": "boolean",
                        "description": "Whether the address parses under RFC 5322 grammar."
                    },
                    "domain": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The ASCII (punycode) domain used for the DNS and list checks."
                    },
                    "mx_found": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Whether the domain has usable mail servers. A null MX counts as false, and so do MX records that only point at loopback, private, or non-existent hosts."
                    },
                    "mx_implicit": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "True when the domain publishes no MX record and mail would fall back to its own address (RFC 5321). Such web-only domains rarely accept mail, so the verdict is risky and no mailbox check runs."
                    },
                    "mx_provider": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Who runs the domain's mail, from its MX hostnames — google, microsoft, zoho, proofpoint, mimecast, and a dozen more, or \"other\" for self-hosted."
                    },
                    "has_spf": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Whether the domain publishes an SPF policy."
                    },
                    "dmarc_policy": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "enum": [
                            "none",
                            "quarantine",
                            "reject",
                            null
                        ],
                        "description": "The domain's DMARC policy, or null when it publishes none."
                    },
                    "domain_registered_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date",
                        "description": "The registration date from public RDAP registry data. Null for free providers and burners (not consulted) and where the registry does not publish it."
                    },
                    "domain_age_days": {
                        "type": [
                            "integer",
                            "null"
                        ],
                        "description": "Days since registration. Under 30 days adds 30 to the risk score, under 180 adds 10."
                    },
                    "disposable": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Whether the domain — or a parent handing out subdomains — is a known burner provider, or routes its mail through one."
                    },
                    "role": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Whether the local part is a role account such as admin@, support@, or billing@."
                    },
                    "free_provider": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Whether the domain is a known free mailbox provider like gmail.com or outlook.com."
                    },
                    "smtp_checked": {
                        "type": "boolean",
                        "description": "Whether a definitive mailbox-level SMTP answer was obtained. When mail servers will not say, it reports false instead of guessing."
                    },
                    "smtp_checked_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "When the mailbox probe actually ran. Confirmed answers are shared across customers for up to a week, so this can predate the lookup."
                    },
                    "mailbox_exists": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Whether the exact inbox exists, from the SMTP-level check. Null when unconfirmed, or when a catch-all domain makes existence unknowable."
                    },
                    "catch_all": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Whether the domain accepts mail for every address, which turns the verdict risky."
                    },
                    "smtp_reason": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "enum": [
                            "no_mailbox",
                            "mailbox_full",
                            "disabled",
                            "greylisted",
                            "timeout",
                            "no_connect",
                            "mx_unreachable",
                            "provider_unavailable",
                            "delivered_recently",
                            null
                        ],
                        "description": "The detail behind the mailbox check. Each value is explained at https://spaw.co/docs/reasons."
                    },
                    "mailbox_confidence": {
                        "type": [
                            "integer",
                            "null"
                        ],
                        "minimum": 0,
                        "maximum": 100,
                        "description": "For catch-all domains and unanswered checks only — a recomputable estimate that the specific mailbox exists, from the signals already in the answer and reported delivery outcomes for the domain. A published heuristic, not a probe."
                    },
                    "did_you_mean": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "A suggested correction (\"mia@gmail.com\" for mia@gmail.con), offered after the mail-server check fails or when the domain is a known typo-squat."
                    },
                    "sources": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Source"
                        },
                        "description": "The datasets and versions behind this answer."
                    }
                }
            },
            "BatchResponse": {
                "type": "object",
                "properties": {
                    "success": {
                        "type": "boolean"
                    },
                    "data": {
                        "type": "object",
                        "properties": {
                            "results": {
                                "type": "array",
                                "description": "One entry per processed address, in input order.",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/EmailResult"
                                        },
                                        "meta": {
                                            "type": "object",
                                            "properties": {
                                                "credits_used": {
                                                    "type": "integer"
                                                },
                                                "cache_hit": {
                                                    "type": "boolean"
                                                },
                                                "suppressed": {
                                                    "type": "boolean"
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "meta": {
                        "type": "object",
                        "properties": {
                            "requested": {
                                "type": "integer",
                                "description": "Addresses in the request."
                            },
                            "processed": {
                                "type": "integer",
                                "description": "Addresses answered; lower than requested when the balance ran out."
                            },
                            "credits_used": {
                                "type": "integer"
                            },
                            "credits_remaining": {
                                "type": "integer"
                            },
                            "stopped_reason": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "enum": [
                                    "insufficient_credits",
                                    null
                                ],
                                "description": "Why the batch stopped early."
                            },
                            "request_id": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            }
                        }
                    }
                }
            },
            "BulkJob": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "queued",
                            "processing",
                            "completed",
                            "failed",
                            "cancelled"
                        ]
                    },
                    "total": {
                        "type": "integer",
                        "description": "Rows in the run."
                    },
                    "duplicate_count": {
                        "type": "integer",
                        "description": "Rows repeating an address already in the list; verified once."
                    },
                    "processed": {
                        "type": "integer"
                    },
                    "deliverable": {
                        "type": "integer"
                    },
                    "risky": {
                        "type": "integer"
                    },
                    "undeliverable": {
                        "type": "integer"
                    },
                    "credits_used": {
                        "type": "integer"
                    },
                    "stopped_reason": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "enum": [
                            "insufficient_credits",
                            null
                        ]
                    },
                    "cancel_requested": {
                        "type": "boolean"
                    },
                    "webhook_status": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "enum": [
                            "delivered",
                            "failed",
                            null
                        ],
                        "description": "Set after the completion webhook was attempted."
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "finished_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    }
                }
            },
            "DomainIntelligence": {
                "type": "object",
                "properties": {
                    "domain": {
                        "type": "string",
                        "description": "The domain as checked — lowercased, punycode form."
                    },
                    "valid": {
                        "type": "boolean",
                        "description": "Whether the input is a syntactically valid hostname. When false, every other field is null."
                    },
                    "mx_found": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Whether the domain can receive mail — MX records, or the RFC 5321 implicit fallback."
                    },
                    "mx_implicit": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "True when the domain has no MX record and mail would fall back to its own address."
                    },
                    "catch_all": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "True once any address at the domain has answered catch-all within the last week; null when not known."
                    },
                    "mx_provider": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The mail infrastructure behind the MX hosts."
                    },
                    "has_spf": {
                        "type": [
                            "boolean",
                            "null"
                        ]
                    },
                    "dmarc_policy": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "enum": [
                            "none",
                            "quarantine",
                            "reject",
                            null
                        ]
                    },
                    "domain_registered_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date"
                    },
                    "domain_age_days": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "disposable": {
                        "type": [
                            "boolean",
                            "null"
                        ]
                    },
                    "free_provider": {
                        "type": [
                            "boolean",
                            "null"
                        ]
                    },
                    "sources": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Source"
                        }
                    }
                }
            },
            "FeedbackItem": {
                "type": "object",
                "required": [
                    "email",
                    "outcome"
                ],
                "properties": {
                    "email": {
                        "type": "string",
                        "maxLength": 254,
                        "description": "The recipient. Normalized like a lookup; an address that does not parse is skipped and counted, not rejected."
                    },
                    "outcome": {
                        "type": "string",
                        "enum": [
                            "delivered",
                            "bounced",
                            "complained"
                        ],
                        "description": "What happened. Report hard bounces only."
                    },
                    "reason": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "maxLength": 255,
                        "description": "The provider's diagnostic, e.g. the SMTP reply. Stored as-is."
                    },
                    "occurred_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time",
                        "description": "When it happened, any parseable date. Defaults to now."
                    }
                }
            },
            "FeedbackSummary": {
                "type": "object",
                "properties": {
                    "window_days": {
                        "type": "integer"
                    },
                    "total": {
                        "type": "integer"
                    },
                    "outcomes": {
                        "type": "object",
                        "properties": {
                            "delivered": {
                                "type": "integer"
                            },
                            "bounced": {
                                "type": "integer"
                            },
                            "complained": {
                                "type": "integer"
                            }
                        }
                    },
                    "by_verdict": {
                        "type": "object",
                        "description": "Outcome counts per verdict on file — deliverable, risky, undeliverable, unknown.",
                        "additionalProperties": {
                            "type": "object",
                            "properties": {
                                "delivered": {
                                    "type": "integer"
                                },
                                "bounced": {
                                    "type": "integer"
                                },
                                "complained": {
                                    "type": "integer"
                                }
                            }
                        }
                    },
                    "measured": {
                        "type": "object",
                        "properties": {
                            "deliverable_delivery_rate": {
                                "type": [
                                    "number",
                                    "null"
                                ],
                                "description": "Delivered ÷ (delivered + bounced) among addresses answered deliverable."
                            },
                            "undeliverable_bounce_rate": {
                                "type": [
                                    "number",
                                    "null"
                                ],
                                "description": "Bounced ÷ (bounced + delivered) among addresses answered undeliverable."
                            }
                        }
                    }
                }
            },
            "Suppression": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "email": {
                        "type": "string",
                        "description": "The normalized address."
                    },
                    "reason": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The verdict reason that suppressed it; null for imports."
                    },
                    "source": {
                        "type": "string",
                        "enum": [
                            "auto",
                            "imported",
                            "feedback"
                        ]
                    },
                    "added_at": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            },
            "Account": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "email": {
                        "type": "string"
                    },
                    "plan": {
                        "type": "string",
                        "example": "free"
                    },
                    "credits": {
                        "type": "object",
                        "properties": {
                            "balance": {
                                "type": "integer"
                            },
                            "monthly_grant": {
                                "type": "integer"
                            }
                        }
                    }
                }
            },
            "PhoneResult": {
                "type": "object",
                "properties": {
                    "valid": {
                        "type": "boolean",
                        "description": "Whether the number is valid for its region."
                    },
                    "reason": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "enum": [
                            "invalid_country_code",
                            "too_short",
                            "too_long",
                            "not_a_number",
                            "invalid_length",
                            "invalid_number",
                            null
                        ],
                        "description": "Why it is not valid."
                    },
                    "e164": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "example": "+14155550142"
                    },
                    "national": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "international": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "country": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ISO 3166-1 alpha-2; null for non-geographic numbers."
                    },
                    "line_type": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "mobile, fixed_line, fixed_line_or_mobile, voip, toll_free, premium_rate, and the other libphonenumber types."
                    },
                    "carrier": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Where the metadata knows it."
                    },
                    "region": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "A geographic description, e.g. \"San Francisco, CA\"."
                    },
                    "extension": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "timezones": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "sources": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Source"
                        }
                    }
                }
            },
            "IpResult": {
                "type": "object",
                "properties": {
                    "ip": {
                        "type": "string",
                        "description": "The canonical form of the address."
                    },
                    "version": {
                        "type": "integer",
                        "enum": [
                            4,
                            6
                        ]
                    },
                    "reason": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "enum": [
                            "reserved_range",
                            null
                        ],
                        "description": "Set when the address is in a reserved or private range."
                    },
                    "country": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "ISO 3166-1 alpha-2."
                    },
                    "region": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "city": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "latitude": {
                        "type": [
                            "number",
                            "null"
                        ]
                    },
                    "longitude": {
                        "type": [
                            "number",
                            "null"
                        ]
                    },
                    "asn": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "org": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "is_datacenter": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Tri-state — null when the list was not consulted."
                    },
                    "is_tor": {
                        "type": [
                            "boolean",
                            "null"
                        ]
                    },
                    "is_vpn": {
                        "type": [
                            "boolean",
                            "null"
                        ]
                    },
                    "risk_score": {
                        "type": [
                            "integer",
                            "null"
                        ],
                        "description": "Tor 70 + datacenter 40 + VPN 30, capped at 100, over the signals actually checked."
                    },
                    "risk_level": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "enum": [
                            "low",
                            "medium",
                            "high",
                            null
                        ]
                    },
                    "sources": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Source"
                        }
                    }
                }
            }
        }
    }
}