Skip to content

Errors

The single most important thing about this API’s status codes:

An address that fails verification is a successful request. undeliverable arrives as a 200 with a negative status. The 4xx and 5xx responses below mean the request was malformed or the service could not answer.

An integration that treats any non-2xx as “bad address” will reject people during our outages and accept nothing during a rate limit. Treat them as what they are: your bug, your balance, or our problem.

{ "error": "insufficient_credits", "message": "the account has nothing left to spend" }

error is a stable machine-readable string. message is prose and may be reworded; do not branch on it.

Status Meaning What to do
400 Malformed body, missing email, an unrecognised variant, or an Idempotency-Key that is not one valid key (invalid_idempotency_key). Fix the request. It will not succeed on retry.
401 Missing or unrecognised credential. See Authentication.
402 insufficient_credits - the account has nothing left to spend. Top up. Deliberately not reported as unknown: what somebody has paid for is not something this service is uncertain about.
403 delegation_not_permitted - the request named an account to charge and this credential may not name one. Remove the header. Ordinary credentials bill their own account.
409 idempotency_conflict - this Idempotency-Key was already used for a different address or depth. Use a new key for a new question. See Idempotency.
413 The request body is larger than permitted. Fix the request.
429 Caller or source budget exceeded, or the request pattern looks like directory harvesting. Back off; honour Retry-After. See Limits.
500 verification_failed - the verification engine failed, and nothing was charged. This says nothing about the address. Retry, then proceed behind a confirmation message.
503 accounting_unavailable - the balance cannot be reached, so no work was done; accounting_paused - accounting is paused, honour Retry-After; or another dependency needed to authorise the request is unavailable. Retry with backoff. Nothing was charged.
503 accounting_uncertain - the charge could not be confirmed. Retry with the same key, or read the operation named in X-Credits-Operation-ID. See Credits.
Status Meaning
400 No identifier supplied.
401 Missing or unrecognised credential.
404 No such verification for this credential. An identifier belonging to another caller answers identically to one that never existed.
500 The verification could not be retrieved.
503 Stored results are unavailable.
Status Meaning
400 missing_domain, or invalid_domain for something no lookup can be made for.
401 Missing or unrecognised credential.
413 The request body is larger than permitted.
429 Caller or source budget exceeded.
503 annotation_unavailable - the reference lists are not loaded. Treat it as no answer, never as a negative one. See The disposable lookup.

There is no 402 here, and its absence is not an omission: the operation spends no credits, so an empty balance cannot refuse it. The same is true of POST /v1/accept-all and POST /v1/public-provider, whose pages list their errors.

The domain lookup has its own table, because its key is mandatory and it has a way of failing the others do not: a charge that could not be confirmed (503 commit_unknown), answered by retrying under the same key.

Status Meaning
401 Missing or unrecognised credential.
404 No such operation for this credential and account.
503 accounting_unavailable - the balance cannot be read right now.

Proceed behind a confirmation message.

This is worth stating flatly because the tempting alternative - fail the signup

  • converts our unavailability into your lost customers, silently. The service itself is built on the same rule: when a dependency is down it degrades to unknown and keeps answering rather than failing, and unknown never blocks a registration. The one exception is the balance - a paid request is refused with a 503 rather than served unpaid - and from your side that 503 is one more reason to proceed behind confirmation, not to refuse the signup. Your side of the integration should hold the same line.

A reasonable default:

let result;
try {
result = await verify(email, key);
} catch {
// Unreachable, timed out, 5xx - all the same thing from here.
result = { status: 'unknown', action: 'allow_with_email_confirmation' };
}

Set a client timeout and treat expiry as unknown. The service will answer pending rather than hold a socket open indefinitely, so a long hang is a network problem rather than a slow verification - and a signup form should not wait on one.