Errors come back as a single object with a type, a stable machine code and a human message. Branch on the code. The message is written for a person reading a log and will be reworded when it can be clearer.
Nothing in this table is billed. An error means the address never reached the verification queue, so your balance is untouched. That is different from an unknown result, which did reach the queue, did cost us a connection, and still is not billed.
Two of these deserve a different response from your code. A 429 means slow down and try again. A 402 means you are out of credits and retrying will not help until someone tops up.
{
"error": {
"type": "invalid_request_error",
"code": "missing_field",
"message": "Field 'email' is required.",
"param": "email",
"request_id": "req_2c8ff1"
}
}Reference
| HTTP | code | What went wrong and whether to retry |
|---|---|---|
| 400 | missing_field | A required field is absent. Retrying the same body fails the same way. |
| 400 | malformed_email | The string is not address-shaped. This is a client bug, not a verdict. |
| 401 | invalid_api_key | No key, wrong key, or a revoked key. Check the prefix matches the environment. |
| 402 | insufficient_credits | Balance too low for the batch. Top up, then resubmit. |
| 404 | not_found | That batch or file id does not exist on this key. |
| 409 | idempotency_conflict | Same idempotency key, different body. Change one of the two. |
| 413 | payload_too_large | Over 100,000 addresses in one body. Use file upload. |
| 429 | rate_limited | Too fast. Back off to Retry-After, then continue. |
| 500 | internal_error | Ours. Retry with backoff and quote the request_id if it persists. |
| 503 | temporarily_unavailable | We are degraded or in maintenance. Retry with backoff. |
request_id is on every error. Quoting it turns a support thread into a two-minute lookup.
Three piles: wait, fix, or call a person
Sort every code into one of three piles and your error handling gets short. Wait and retry covers 429, 500 and 503. Fix the request covers 400, 404, 409 and 413, where sending the same thing again can't work. Call a person covers 401 and 402, because no code change or pause will help.
Say your nightly cleaning job hits insufficient_credits at 2 a.m. The wrong design retries fifty times and gives up with the list dropped. A better one keeps the list queued and alerts whoever buys credits, so the job picks up where it left off after the top-up.
A 401 right after a key rotation deserves the same treatment. Something is still holding the old key, and only a person can find out which service it is.
What to log, and what to leave out
Log five things for every error: the HTTP status, code, param when it's present, request_id, and your own job or batch name. With those, a support thread becomes a lookup. Leave out the Authorization header, since a log line holding your key turns a small bug into a rotation.
Keep malformed_email out of your verdict counts. It means your client sent something that wasn't shaped like an address, which is a bug on your side and says nothing about a mailbox. Run a basic shape check before you call, and if these errors keep arriving, read a sample of the strings. You'll often find a column mix-up behind them.
New codes can appear over time, so give your handler a default branch. One sensible fallback is to go by the status class when you don't know the code. A 4xx means don't retry, and a 5xx means retry with backoff. Your client keeps working on the day we add one.
Questions developers ask
Do errors cost credits?
No. Only a definitive verdict is billed, and an error is not a verdict.
Is the message string stable?
No, and do not parse it. code is the contract. Messages get reworded when a clearer sentence exists.
What is the difference between an error and an unknown?
An error means we could not run the check. An unknown means we ran it and the receiving server refused to answer clearly. One is free because nothing happened; the other is free because guessing would be dishonest.