API reference

Every example here is a runnable curl.

No signup wall in front of anything. Documentation is the acquisition channel for a developer API, and a wall in front of it is a wall in front of the funnel.

Authentication

One key, one host, one wallet.

Send it as a bearer token. Keys are prefixed so a leak is recognizable by eye in a log line or a public repository. We store a hash and the first twelve characters, and the secret is shown once.

Authorization: Bearer wk_live_...

Endpoints

Three, one of which is the sum of the others.

POST /v1/email

POST /v1/email

Syntax, provider rules, typo, disposable domains, role addresses, MX, SMTP probe and catch-all, cheapest check first.

Request

curl https://api.webkitapi.dev/v1/email \
  -H "Authorization: Bearer $WEBKIT_KEY" \
  -d '{"email": "john.doe@gmial.com"}'

Response

{
  "email": "john.doe@gmial.com",
  "verdict": "undeliverable",
  "reasons": ["domain_typo"],
  "suggestion": "john.doe@gmail.com",
  "flags": {"disposable": null, "role": null, "catch_all": null},
  "checks": {
    "syntax": "pass", "provider_rules": "pass", "typo": "fail",
    "disposable": "skipped", "role": "skipped", "mx": "skipped",
    "smtp": "skipped", "catch_all": "skipped"
  },
  "duration_ms": 9
}
POST /v1/address

POST /v1/address

Does this address exist and can it receive mail. Takes a free-form line or separate fields. France, on open data.

Request

curl https://api.webkitapi.dev/v1/address \
  -H "Authorization: Bearer $WEBKIT_KEY" \
  -d '{"line": "3 Rue de Rivolli, 75004 Paris"}'

Response

{
  "verdict": "corrected",
  "reasons": ["street_corrected"],
  "normalized": {
    "house_number": "3",
    "house_number_suffix": null,
    "street": "Rue de Rivoli",
    "postcode": "75004",
    "city": "Paris",
    "country": "FR",
    "line": "3 Rue de Rivoli, 75004 Paris",
    "latitude": 48.855604,
    "longitude": 2.358982
  },
  "duration_ms": 23
}
POST /v1/signup

POST /v1/signup

Everything that applies, in parallel, as one verdict with the per-signal detail underneath. Billed as the sum of its parts.

Request

curl https://api.webkitapi.dev/v1/signup \
  -H "Authorization: Bearer $WEBKIT_KEY" \
  -d '{"email": "someone@mailinator.com",
       "address": {"line": "3 Rue de Rivoli, 75004 Paris"}}'

Response

{
  "verdict": "unknown",
  "reasons": ["email.disposable_domain", "email.catch_all_domain"],
  "signals": {
    "email": {
      "verdict": "unknown",
      "reasons": ["disposable_domain", "catch_all_domain"],
      "suggestion": null
    },
    "address": {
      "verdict": "deliverable",
      "reasons": [],
      "normalized": "3 Rue de Rivoli, 75004 Paris"
    }
  },
  "duration_ms": 930
}
POST /v1/email/bulk

POST /v1/email/bulk

Submits a batch and returns a job id. Asynchronous out of necessity: the SMTP probe is rate limited per receiving provider, so a synchronous version would time out on exactly the largest lists. Addresses deduplicated, quota charged per address at submission.

Request

curl https://api.webkitapi.dev/v1/email/bulk \
  -H "Authorization: Bearer $WEBKIT_KEY" \
  -d '{"emails": ["a@example.com", "b@gmial.com"]}'

Response

{
  "job_id": "01a02995-...",
  "status": "queued",
  "total": 2,
  "poll": "https://api.webkitapi.dev/v1/jobs/01a02995-...",
  "expires_at": "2026-08-24T13:08:00+00:00"
}
GET /v1/jobs/{id}

GET /v1/jobs/{id}

Progress and the results already verified, so you do not wait for the last address to see the first thousand. Free: it costs no quota, and it has its own rate allowance of 60 calls a minute rather than spending the one your verification calls need. Submitted addresses and results deleted 48 hours after submission.

Request

curl https://api.webkitapi.dev/v1/jobs/01a02995-... \
  -H "Authorization: Bearer $WEBKIT_KEY"

Response

{
  "status": "completed",
  "total": 2,
  "processed": 2,
  "expires_at": "2026-08-24T13:08:00+00:00",
  "results": [
    {"email": "b@gmial.com", "verdict": "undeliverable",
     "reasons": ["domain_typo"], "suggestion": "b@gmail.com"}
  ]
}

Errors

One shape for every error.

application/problem+json per RFC 9457, across the whole platform.

SituationStatusType
Key missing or malformed 401 invalid-api-key
Key revoked 401 revoked-api-key
Key not allowed on this endpoint 403 endpoint-not-allowed
Malformed payload 400 invalid-request
Per-minute rate exceeded 429 rate-limit-exceeded
Monthly quota exhausted 402 quota-exceeded
Batch too large 413 batch-too-large
Job unknown, expired, or owned by another account 404 job-not-found
The request could not be served as sent 400 request-failed
A dependency of ours is unreachable 503 service-unavailable
A bug on our side 500 internal-error

402 for the quota, 429 for the rate. Slowing down and running out are two different problems with two different solutions. A client that retries on 429 must not retry on 402: the month will not reset any sooner.

Quota headers

Sent on every response, not just the last one.

Knowing where you stand should not require polling a dashboard, and it costs a Redis read that has already happened.

X-RateLimit-Limit: 30
X-RateLimit-Remaining: 27
X-RateLimit-Reset: 1787347190
X-Quota-Limit: 100
X-Quota-Remaining: 42
X-Quota-Reset: 1788220800

X-RateLimit-Limit is the ceiling actually enforced, sustained rate plus burst. Publishing the lower number while enforcing the higher one would make the header useless in the direction that matters.

Test mode

Reserved addresses that never touch the network.

A wk_test_ key answers from these addresses and is never counted, so your integration suite runs with no bill and without spending our sending reputation.

AddressVerdict
deliverable@test.webkitapi.devdeliverable
undeliverable@test.webkitapi.devundeliverable, mailbox_not_found
catchall@test.webkitapi.devunknown, catch_all_domain
disposable@test.webkitapi.devrisky, disposable_domain
greylisted@test.webkitapi.devunknown, greylisted

The address endpoint has its own reserved set. Postcode 00000 belongs to no French commune, so these five lines answer the same way on every install whatever the register holds.

AddressVerdict
1 rue Deliverable, 00000 Testvilledeliverable
1 rue Corected, 00000 Testvillecorrected, street_corrected
1 rue Nonexistent, 00000 Testvillenonexistent, street_not_found
1 rue Unknown, 00000 Testvilleunknown, postcode_not_covered
rue Deliverable, 00000 Testvilleincomplete, missing_house_number