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
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
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
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
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}
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.
| Situation | Status | Type |
|---|---|---|
| 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.
| Address | Verdict |
|---|---|
deliverable@test.webkitapi.dev | deliverable |
undeliverable@test.webkitapi.dev | undeliverable, mailbox_not_found |
catchall@test.webkitapi.dev | unknown, catch_all_domain |
disposable@test.webkitapi.dev | risky, disposable_domain |
greylisted@test.webkitapi.dev | unknown, 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.
| Address | Verdict |
|---|---|
1 rue Deliverable, 00000 Testville | deliverable |
1 rue Corected, 00000 Testville | corrected, street_corrected |
1 rue Nonexistent, 00000 Testville | nonexistent, street_not_found |
1 rue Unknown, 00000 Testville | unknown, postcode_not_covered |
rue Deliverable, 00000 Testville | incomplete, missing_house_number |