Credits

GET /v1/credits

GET /credits returns a balance and a ledger. The balance is what you can spend right now. The ledger is every purchase and every deduction, so a disputed invoice is a lookup rather than an argument.

Credits never expire. There is no twelve-month clock, no rolling window, no use-it-or-lose-it. Buy a million, spend them across three years, and the last one costs what the first one did.

What spends a credit: one definitive result on one distinct address, and the definitive results are valid, invalid and catch_all. What does not: unknown results, duplicates inside a batch, rows rejected on syntax alone, errors, rate-limited requests, and every call made with a sandbox key.

Shell
curl https://api.zapbounce.com/v1/credits \
  -H "Authorization: Bearer zb_live_..."
JSON
{
  "balance": 48210,
  "free_monthly_remaining": 74,
  "free_monthly_resets_at": "2026-10-01T00:00:00Z",
  "expires_at": null,
  "low_balance_threshold": 5000,
  "ledger": [
    { "at": "2026-09-18T10:19:12Z", "delta": -11220, "reason": "batch bat_8f2c1d" },
    { "at": "2026-09-14T08:41:03Z", "delta": 100000, "reason": "purchase 100k pack" }
  ]
}

Fields

FieldTypeWhat it carries
balanceintegerPaid credits available. Never decreases on its own.
free_monthly_remainingintegerOf the 100 free checks a month. Resets on the first; no card involved.
expires_atnullAlways null. It exists so a client written against a vendor with expiry has somewhere to look.
low_balance_thresholdintegerCrossing it fires the credits.low webhook.

Matching a batch to the ledger, line by line

Say your finance lead asks why a 32,500-row file cost 28,900 credits. The batch status call has the answer. Of those rows, 2,500 were repeats, which leaves unique at 30,000. Another 1,100 came back unknown and weren't billed. Take both away and you get 28,900.

Now open the ledger. You should find one entry with a delta of minus 28,900 and a reason that names the batch. Your balance before the run was 75,000, so it should now read 46,100. If all three numbers agree, the question is closed in about two minutes.

A mismatch is worth a support ticket, and the ledger entry plus the batch id is all the evidence you'll need to send.

Guarding against a 402 in the middle of the night

A balance isn't a reservation, so two batches that start together can both pass your check and one can still fail. You have two ways around that. Submit batches one at a time from a single queue. Or set low_balance_threshold to cover the largest set of batches you'd ever run in one day.

Add a preflight to your job. Read the balance, compare it with the row count of the file, and refuse to submit if the balance is lower. The row count is a safe ceiling, because repeats and unknowns only ever bring the real cost down.

Keep free_monthly_remaining out of that sum. This reference doesn't say whether free checks are spent before paid ones, so don't build on either order. Treat the free allowance as a bonus for small manual checks, and plan real jobs against balance alone.

Send the credits.low event somewhere a human will read it. A quiet channel nobody opens is the same as having no alert.

Questions developers ask

Do credits expire?

Never. Kickbox and Hunter expire theirs at twelve months; we do not have a clock.

Does a catch-all result cost a credit?

Yes. It took a full handshake plus a second probe, and it tells you something true about the domain. The billed field on each result row is the source of truth, and a batch's billed count is unique minus unknown.

What is the 100 free credits a month?

A recurring monthly allowance, no card required. It resets on the first and does not roll over, which is why it sits in its own field.

How do I avoid a batch failing on balance?

Set low_balance_threshold above your typical batch size and handle the credits.low webhook. Finding out at submission time is finding out late.

Try it against a sandbox key

Scripted verdicts, no SMTP connections, no credits. Live keys come with 100 free checks a month and no card.