{
    "openapi": "3.1.0",
    "info": {
        "title": "webkitapi.dev",
        "version": "1.0.0",
        "description": "APIs that answer one question: is this real? Every response carries the reasons behind its verdict. There is no confidence score.",
        "license": {
            "name": "Proprietary"
        }
    },
    "servers": [
        {
            "url": "https://api.webkitapi.dev"
        }
    ],
    "security": [
        {
            "bearerAuth": []
        }
    ],
    "paths": {
        "/v1/email": {
            "post": {
                "operationId": "verifyEmail",
                "summary": "Verify an email address",
                "description": "Runs the checks cheapest first and stops as soon as the answer is settled. A malformed address never costs a DNS lookup, and a suspected typo is reported rather than probed.",
                "tags": [
                    "email"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "email"
                                ],
                                "properties": {
                                    "email": {
                                        "type": "string",
                                        "maxLength": 320,
                                        "examples": [
                                            "john.doe@gmial.com"
                                        ]
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "A verdict. Note that unknown is a successful answer, not a failure.",
                        "headers": {
                            "X-RateLimit-Limit": {
                                "$ref": "#/components/headers/RateLimitLimit"
                            },
                            "X-RateLimit-Remaining": {
                                "$ref": "#/components/headers/RateLimitRemaining"
                            },
                            "X-Quota-Limit": {
                                "$ref": "#/components/headers/QuotaLimit"
                            },
                            "X-Quota-Remaining": {
                                "$ref": "#/components/headers/QuotaRemaining"
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/EmailVerification"
                                }
                            }
                        }
                    },
                    "400": {
                        "$ref": "#/components/responses/InvalidRequest"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/QuotaExceeded"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    },
                    "503": {
                        "$ref": "#/components/responses/ServiceUnavailable"
                    }
                }
            }
        },
        "/v1/address": {
            "post": {
                "operationId": "verifyAddress",
                "summary": "Verify a postal address",
                "description": "Does this address exist and can it receive mail. Accepts a free-form line or separate fields. Outside covered countries the verdict is unknown rather than a guess.",
                "tags": [
                    "address"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/AddressInput"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "A verdict. Note that unknown is a successful answer, not a failure.",
                        "headers": {
                            "X-RateLimit-Limit": {
                                "$ref": "#/components/headers/RateLimitLimit"
                            },
                            "X-RateLimit-Remaining": {
                                "$ref": "#/components/headers/RateLimitRemaining"
                            },
                            "X-Quota-Limit": {
                                "$ref": "#/components/headers/QuotaLimit"
                            },
                            "X-Quota-Remaining": {
                                "$ref": "#/components/headers/QuotaRemaining"
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/AddressVerification"
                                }
                            }
                        }
                    },
                    "400": {
                        "$ref": "#/components/responses/InvalidRequest"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/QuotaExceeded"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    },
                    "503": {
                        "$ref": "#/components/responses/ServiceUnavailable"
                    }
                }
            }
        },
        "/v1/signup": {
            "post": {
                "operationId": "verifySignup",
                "summary": "Check every applicable signal at once",
                "description": "One combined verdict, the worst of its parts rather than an average, with the per-signal breakdown underneath. Billed as the sum of its parts.",
                "tags": [
                    "signup"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "email": {
                                        "type": "string",
                                        "maxLength": 320
                                    },
                                    "address": {
                                        "$ref": "#/components/schemas/AddressInput"
                                    },
                                    "country": {
                                        "$ref": "#/components/schemas/CountryCode"
                                    }
                                },
                                "description": "At least one of email or address is required."
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "A verdict. Note that unknown is a successful answer, not a failure.",
                        "headers": {
                            "X-RateLimit-Limit": {
                                "$ref": "#/components/headers/RateLimitLimit"
                            },
                            "X-RateLimit-Remaining": {
                                "$ref": "#/components/headers/RateLimitRemaining"
                            },
                            "X-Quota-Limit": {
                                "$ref": "#/components/headers/QuotaLimit"
                            },
                            "X-Quota-Remaining": {
                                "$ref": "#/components/headers/QuotaRemaining"
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SignupVerification"
                                }
                            }
                        }
                    },
                    "400": {
                        "$ref": "#/components/responses/InvalidRequest"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/QuotaExceeded"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    },
                    "503": {
                        "$ref": "#/components/responses/ServiceUnavailable"
                    }
                }
            }
        },
        "/v1/email/bulk": {
            "post": {
                "operationId": "verifyEmailsInBulk",
                "summary": "Submit a batch for asynchronous verification",
                "description": "Returns 202 with a job id. Bulk has to be asynchronous: the SMTP probe is paced per receiving provider and takes seconds per unknown domain, so a synchronous bulk endpoint would time out on exactly the largest lists. Addresses are deduplicated on submission, and quota is charged per address at that point rather than on completion.",
                "tags": [
                    "email"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "emails"
                                ],
                                "properties": {
                                    "emails": {
                                        "type": "array",
                                        "items": {
                                            "type": "string",
                                            "maxLength": 320
                                        },
                                        "maxItems": 5000
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "202": {
                        "description": "Accepted. Poll the returned URL for progress and results.",
                        "headers": {
                            "Location": {
                                "schema": {
                                    "type": "string",
                                    "format": "uri"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/JobAccepted"
                                }
                            }
                        }
                    },
                    "400": {
                        "$ref": "#/components/responses/InvalidRequest"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "402": {
                        "$ref": "#/components/responses/QuotaExceeded"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "413": {
                        "$ref": "#/components/responses/BatchTooLarge"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    },
                    "503": {
                        "$ref": "#/components/responses/ServiceUnavailable"
                    }
                }
            }
        },
        "/v1/jobs/{id}": {
            "get": {
                "operationId": "getJob",
                "summary": "Read a bulk job",
                "description": "Returns status, progress and the results verified so far, so a caller watching a long list sees the first thousand without waiting for the last address. Submitted addresses and results are deleted 48 hours after submission.",
                "tags": [
                    "email"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "format": "uuid"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The job.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Job"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "404": {
                        "$ref": "#/components/responses/JobNotFound"
                    },
                    "429": {
                        "$ref": "#/components/responses/RateLimited"
                    },
                    "503": {
                        "$ref": "#/components/responses/ServiceUnavailable"
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "bearerAuth": {
                "type": "http",
                "scheme": "bearer",
                "description": "One key across every endpoint. Prefixed wk_live_ or wk_test_ so a leak is identifiable on sight."
            }
        },
        "schemas": {
            "Verdict": {
                "type": "string",
                "enum": [
                    "deliverable",
                    "undeliverable",
                    "risky",
                    "unknown"
                ],
                "description": "Four values, each defined. There is deliberately no confidence percentage: an uncalibrated number is a claim we cannot support, and it pushes the threshold decision onto the caller."
            },
            "Reason": {
                "type": "string",
                "enum": [
                    "invalid_syntax",
                    "local_part_too_long",
                    "domain_too_long",
                    "provider_rule_violation",
                    "domain_typo",
                    "no_mx_record",
                    "domain_does_not_resolve",
                    "mailbox_not_found",
                    "mailbox_full",
                    "disposable_domain",
                    "role_address",
                    "catch_all_domain",
                    "greylisted",
                    "probe_timeout",
                    "probe_unavailable"
                ]
            },
            "CheckName": {
                "type": "string",
                "enum": [
                    "syntax",
                    "provider_rules",
                    "typo",
                    "disposable",
                    "role",
                    "mx",
                    "smtp",
                    "catch_all"
                ]
            },
            "CheckStatus": {
                "type": "string",
                "enum": [
                    "pass",
                    "fail",
                    "inconclusive",
                    "skipped"
                ],
                "description": "skipped means the check never ran because an earlier one settled the answer, which is not the same as passing."
            },
            "AddressVerdict": {
                "type": "string",
                "enum": [
                    "deliverable",
                    "corrected",
                    "incomplete",
                    "nonexistent",
                    "unknown"
                ]
            },
            "AddressReason": {
                "type": "string",
                "enum": [
                    "street_not_found",
                    "house_number_out_of_range",
                    "postcode_city_mismatch",
                    "postcode_corrected",
                    "city_corrected",
                    "street_corrected",
                    "missing_house_number",
                    "missing_street",
                    "missing_postcode",
                    "missing_city",
                    "country_not_covered",
                    "postcode_not_covered"
                ]
            },
            "CountryCode": {
                "type": "string",
                "enum": [
                    "FR"
                ],
                "description": "Countries with an open address register behind them. Elsewhere the verdict is unknown."
            },
            "Endpoint": {
                "type": "string",
                "enum": [
                    "email",
                    "address",
                    "signup"
                ]
            },
            "AddressInput": {
                "type": "object",
                "properties": {
                    "line": {
                        "type": "string",
                        "description": "The whole address as one string."
                    },
                    "house_number": {
                        "type": "string"
                    },
                    "street": {
                        "type": "string"
                    },
                    "postcode": {
                        "type": "string"
                    },
                    "city": {
                        "type": "string"
                    },
                    "country": {
                        "$ref": "#/components/schemas/CountryCode"
                    }
                }
            },
            "EmailVerification": {
                "type": "object",
                "required": [
                    "email",
                    "verdict",
                    "reasons",
                    "checks",
                    "duration_ms"
                ],
                "properties": {
                    "email": {
                        "type": "string"
                    },
                    "verdict": {
                        "$ref": "#/components/schemas/Verdict"
                    },
                    "reasons": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Reason"
                        }
                    },
                    "suggestion": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "A proposed correction. Never apply it automatically."
                    },
                    "flags": {
                        "type": "object",
                        "properties": {
                            "disposable": {
                                "type": [
                                    "boolean",
                                    "null"
                                ]
                            },
                            "role": {
                                "type": [
                                    "boolean",
                                    "null"
                                ]
                            },
                            "catch_all": {
                                "type": [
                                    "boolean",
                                    "null"
                                ]
                            }
                        },
                        "description": "null means the check never ran, which is not the same as false."
                    },
                    "checks": {
                        "type": "object",
                        "additionalProperties": {
                            "$ref": "#/components/schemas/CheckStatus"
                        }
                    },
                    "duration_ms": {
                        "type": "integer"
                    }
                }
            },
            "AddressVerification": {
                "type": "object",
                "required": [
                    "verdict",
                    "reasons",
                    "duration_ms"
                ],
                "properties": {
                    "verdict": {
                        "$ref": "#/components/schemas/AddressVerdict"
                    },
                    "reasons": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/AddressReason"
                        }
                    },
                    "normalized": {
                        "type": [
                            "object",
                            "null"
                        ],
                        "properties": {
                            "house_number": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "house_number_suffix": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "street": {
                                "type": "string"
                            },
                            "postcode": {
                                "type": "string"
                            },
                            "city": {
                                "type": "string"
                            },
                            "country": {
                                "$ref": "#/components/schemas/CountryCode"
                            },
                            "line": {
                                "type": "string"
                            },
                            "latitude": {
                                "type": [
                                    "number",
                                    "null"
                                ]
                            },
                            "longitude": {
                                "type": [
                                    "number",
                                    "null"
                                ]
                            }
                        }
                    },
                    "duration_ms": {
                        "type": "integer"
                    }
                }
            },
            "SignupVerification": {
                "type": "object",
                "required": [
                    "verdict",
                    "reasons",
                    "signals",
                    "duration_ms"
                ],
                "properties": {
                    "verdict": {
                        "$ref": "#/components/schemas/Verdict"
                    },
                    "reasons": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "Prefixed with the signal they came from, for example email.disposable_domain."
                    },
                    "signals": {
                        "type": "object",
                        "description": "Only the signals that ran are present, and only those are billed.",
                        "properties": {
                            "email": {
                                "$ref": "#/components/schemas/EmailSignal"
                            },
                            "address": {
                                "$ref": "#/components/schemas/AddressSignal"
                            }
                        },
                        "additionalProperties": false
                    },
                    "duration_ms": {
                        "type": "integer"
                    }
                }
            },
            "EmailSignal": {
                "type": "object",
                "required": [
                    "verdict",
                    "reasons",
                    "suggestion"
                ],
                "properties": {
                    "verdict": {
                        "$ref": "#/components/schemas/Verdict"
                    },
                    "reasons": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Reason"
                        }
                    },
                    "suggestion": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "A proposed correction. Never apply it automatically."
                    }
                }
            },
            "AddressSignal": {
                "type": "object",
                "required": [
                    "verdict",
                    "reasons",
                    "normalized"
                ],
                "properties": {
                    "verdict": {
                        "$ref": "#/components/schemas/AddressVerdict"
                    },
                    "reasons": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/AddressReason"
                        }
                    },
                    "normalized": {
                        "type": [
                            "string",
                            "null"
                        ]
                    }
                }
            },
            "JobAccepted": {
                "type": "object",
                "required": [
                    "job_id",
                    "status",
                    "total",
                    "poll",
                    "expires_at"
                ],
                "properties": {
                    "job_id": {
                        "type": "string",
                        "format": "uuid"
                    },
                    "status": {
                        "$ref": "#/components/schemas/JobStatus"
                    },
                    "total": {
                        "type": "integer",
                        "description": "Addresses accepted after deduplication."
                    },
                    "poll": {
                        "type": "string",
                        "format": "uri"
                    },
                    "expires_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "retention_note": {
                        "type": "string"
                    }
                }
            },
            "JobStatus": {
                "type": "string",
                "enum": [
                    "queued",
                    "running",
                    "completed",
                    "failed"
                ]
            },
            "Job": {
                "type": "object",
                "required": [
                    "job_id",
                    "status",
                    "total",
                    "processed",
                    "results"
                ],
                "properties": {
                    "job_id": {
                        "type": "string",
                        "format": "uuid"
                    },
                    "status": {
                        "$ref": "#/components/schemas/JobStatus"
                    },
                    "total": {
                        "type": "integer"
                    },
                    "processed": {
                        "type": "integer"
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "expires_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "failure_reason": {
                        "type": "string"
                    },
                    "results": {
                        "type": "array",
                        "description": "Partial while the job runs, complete once it finishes.",
                        "items": {
                            "$ref": "#/components/schemas/EmailVerification"
                        }
                    }
                }
            },
            "Problem": {
                "type": "object",
                "description": "RFC 9457 problem details. One shape for every failure on the platform.",
                "required": [
                    "type",
                    "title",
                    "status",
                    "detail"
                ],
                "properties": {
                    "type": {
                        "type": "string",
                        "format": "uri",
                        "enum": [
                            "https://webkitapi.dev/errors/invalid-api-key",
                            "https://webkitapi.dev/errors/revoked-api-key",
                            "https://webkitapi.dev/errors/endpoint-not-allowed",
                            "https://webkitapi.dev/errors/invalid-request",
                            "https://webkitapi.dev/errors/rate-limit-exceeded",
                            "https://webkitapi.dev/errors/quota-exceeded",
                            "https://webkitapi.dev/errors/batch-too-large",
                            "https://webkitapi.dev/errors/job-not-found",
                            "https://webkitapi.dev/errors/request-failed",
                            "https://webkitapi.dev/errors/service-unavailable",
                            "https://webkitapi.dev/errors/internal-error"
                        ]
                    },
                    "title": {
                        "type": "string"
                    },
                    "status": {
                        "type": "integer"
                    },
                    "detail": {
                        "type": "string"
                    }
                }
            }
        },
        "responses": {
            "InvalidRequest": {
                "description": "Malformed payload.",
                "content": {
                    "application/problem+json": {
                        "schema": {
                            "$ref": "#/components/schemas/Problem"
                        }
                    }
                }
            },
            "Unauthorized": {
                "description": "Missing, malformed or revoked key.",
                "content": {
                    "application/problem+json": {
                        "schema": {
                            "$ref": "#/components/schemas/Problem"
                        }
                    }
                }
            },
            "Forbidden": {
                "description": "This key is not allowed to call this endpoint.",
                "content": {
                    "application/problem+json": {
                        "schema": {
                            "$ref": "#/components/schemas/Problem"
                        }
                    }
                }
            },
            "QuotaExceeded": {
                "description": "The monthly allowance is spent. Retrying will not help, and a client that treats this as backoff will hammer us until the first of the month.",
                "content": {
                    "application/problem+json": {
                        "schema": {
                            "$ref": "#/components/schemas/Problem"
                        }
                    }
                }
            },
            "BatchTooLarge": {
                "description": "More addresses in one job than the plan allows. The problem document carries the limit that applied.",
                "content": {
                    "application/problem+json": {
                        "schema": {
                            "$ref": "#/components/schemas/Problem"
                        }
                    }
                }
            },
            "JobNotFound": {
                "description": "Unknown, expired, or owned by another account. The three are deliberately indistinguishable: a job id is a handle on somebody customer list.",
                "content": {
                    "application/problem+json": {
                        "schema": {
                            "$ref": "#/components/schemas/Problem"
                        }
                    }
                }
            },
            "RateLimited": {
                "description": "Too fast. Retry after the Retry-After header.",
                "headers": {
                    "Retry-After": {
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Seconds."
                    }
                },
                "content": {
                    "application/problem+json": {
                        "schema": {
                            "$ref": "#/components/schemas/Problem"
                        }
                    }
                }
            },
            "ServiceUnavailable": {
                "description": "A dependency is unreachable. Temporary, and safe to retry after the Retry-After header.",
                "headers": {
                    "Retry-After": {
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Seconds."
                    }
                },
                "content": {
                    "application/problem+json": {
                        "schema": {
                            "$ref": "#/components/schemas/Problem"
                        }
                    }
                }
            }
        },
        "headers": {
            "RateLimitLimit": {
                "schema": {
                    "type": "integer"
                },
                "description": "The ceiling actually enforced: sustained rate plus burst."
            },
            "RateLimitRemaining": {
                "schema": {
                    "type": "integer"
                }
            },
            "QuotaLimit": {
                "schema": {
                    "type": "integer"
                },
                "description": "Monthly allowance for this endpoint. Absent on unmetered keys."
            },
            "QuotaRemaining": {
                "schema": {
                    "type": "integer"
                },
                "description": "Sent on every response, not only when it runs out."
            }
        }
    }
}
