Home / Developers / Errors, credits and rate limits

Errors, credits and rate limits

What each status code means, when credits are charged and refunded, and how to handle a 202 pending response without paying twice.

Updated 2026-10-18

TL;DR

  • Errors are JSON: {"error": "message"} with a status of 400, 401, 402, 404 or 429. Retry only 429 (and network failures). Fix the rest.
  • Credits are charged when you submit, one per unique address. Duplicates are free and unknown results are refunded when the list finishes.
  • A 202 pending from /verify is not an error. The check is running and paid for, so read the result from the list. Never post the address again.

Status codes

Every endpoint under https://mailexchange.com/api/v1 answers errors in the same shape. The message strings below are what the API returns today. Match on the status code, not the text.

StatusMeaningExample messageWhat to do
200 / 201OK / list createdCarry on.
202Single check still running{"state": "pending", "list_id": 4821}Poll the list. Do not resend.
400Bad requestemail required, emails required, max 100000 addresses per list, callback_url must be https, invalid JSONFix the request. Do not retry unchanged.
401Key missing or wrongmissing or invalid API keyCheck the Authorization header and the key.
402Not enough creditsinsufficient credits: 500 neededStop and top up. Nothing was created or charged.
404Unknown list or routelist not found, not foundCheck the ID. Lists belong to the key's account.
429Rate limitrate limit exceededBack off and retry.

Treat a 5xx or a network error as temporary and retry a few times with backoff, but see the note on timeouts below before blindly repeating a submit. Language examples of all this are in Python, Node.js, PHP, Ruby, Go, Java and C#.

How credits work

  • Charged at submission. POST /lists takes one credit per unique address immediately. POST /verify does the same for its one address, because a single check is a one-address list.
  • Duplicates are free. Addresses are trimmed and lower-cased, and repeats inside the list are dropped and not charged. The summary shows them as duplicates_removed. This applies within one list. Submitting the same address in two separate calls charges twice.
  • Unknowns are refunded. When a list finishes, one credit comes back for every result with the state unknown, so you only pay for answers. For the unknown reasons see email verification statuses.
  • Check your balance. GET /v1/account returns your name, credits, credits_used and the number of lists.
curl -H "Authorization: Bearer $MAILEXCHANGE_API_KEY" https://mailexchange.com/api/v1/account

Credits never expire, and the price per credit is on the pricing page. A 402 on a big list means the whole list was refused, not partly processed, so you can top up and submit again.

Rate limits

Limits are per API key over a rolling 60 second window. POST /verify counts one per request. POST /lists counts one per address in the list, with a much larger allowance, so a bulk submit is the right tool for volume. If you exceed either you get 429. A rate-limited call is rejected before any work happens, so it is always safe to repeat it.

The practical rules: reuse connections, use lists for more than a few addresses, poll every 5 to 10 seconds rather than in a tight loop, and add a retry with exponential backoff (1, 2, 4, 8 seconds is plenty). If you regularly need a higher limit, talk to us.

Handling 202 pending idempotently

Most single checks finish within seconds. If the mail server is slow, /verify waits about 20 seconds and then returns 202. That response means the check exists, has a list_id and was already charged. The safe pattern is:

  1. Save the list_id from the 202 response next to your record for that address.
  2. Poll GET /v1/lists/{list_id}/results?limit=1 every few seconds. While the row's state is pending, keep waiting.
  3. When the state changes, use that result. If you stop waiting, look again later with the same list_id instead of calling /verify again.
# Python, using the call() helper from the Python guide
r = call("POST", "/verify", json={"email": email})
result = r.json()
if r.status_code == 202:
    store_pending(email, result["list_id"])        # remember it, never re-post
else:
    store_result(email, result)

# later, from a worker:
rows = call("GET", f"/lists/{list_id}/results", params={"limit": 1}).json()["results"]
if rows and rows[0]["state"] != "pending":
    store_result(email, rows[0])
Timeouts. There is no idempotency-key header. If your own HTTP client times out on a submit, you cannot tell whether the list was created. Set a generous timeout (45 seconds covers the 20 second wait on /verify), and if you still time out, compare GET /account before and after to see whether credits were taken, rather than resubmitting blindly.

A simple error policy

  • 429: retry with backoff, up to about 4 times, then give up and queue the work.
  • 402: alert a human, pause the job, resume after a top-up.
  • 401: alert and stop. Retrying never fixes a bad key.
  • 400, 404: a bug in the request or a wrong ID. Log the message and the payload.
  • 5xx or network error: retry a few times, as above.
  • Unknown or pending result: retry the address later. Unknowns are refunded, and resubmitting the address is a new charge.

Questions

Do I pay twice if I retry a /verify that returned 202?

Yes, if you post the address again, because that starts a new check. Read the result of the first one from /lists/{list_id}/results instead.

Am I charged for unknown results?

No. Unknown results are refunded automatically when the list finishes. Duplicates inside a list are never charged.

What should I do on a 429?

Wait and retry with exponential backoff. A rate-limited request does no work and costs no credits.

Related