Authentication is a bearer token in the Authorization header. You create a key in the dashboard, you send it on every request, and there is no second step: no OAuth dance, no signed request, no per-call nonce.
Keys carry a prefix so you can tell at a glance which one leaked into a log. Live keys start zb_live_, sandbox keys start zb_test_. A sandbox key returns shaped results without opening a single SMTP connection and never spends a credit, which makes it the right key for your CI pipeline.
We do not accept the key as a query parameter. Query strings end up in access logs, in browser history, and in the Referer header of any page you redirect to. The header costs you nothing and keeps the secret out of all three.
curl https://api.zapbounce.com/v1/verify \
-H "Authorization: Bearer zb_live_..." \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com"}'{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "No key matches that token."
}
}Fields
| Field | Type | What it carries |
|---|---|---|
| Authorization | header | Bearer plus the key. Required on every request including /account. |
| Idempotency-Key | header | Optional. Safe retries on writes. See the idempotency page. |
| ZapBounce-Version | header (provisional) | Pins the response shape to a dated version. The header name is still under review. |
A field marked provisional could still be renamed before v1 is declared stable. Every other field is fixed.
Three jobs, three keys, one Friday leak
Say you run a signup form, a nightly list-cleaning job and a CI suite. Give each one its own key. CI gets a zb_test_ key, so test runs never open a connection or spend a credit. The form and the nightly job each get a zb_live_ key with a label you'll recognize later.
Now picture the nightly job's key showing up in a pasted log on a Friday afternoon. You create a replacement, deploy it to that one job, watch the old key's last-used timestamp go quiet, and revoke it. Your signup form never noticed, because it never shared that key. With a single shared key, the same leak means a rushed deploy across every service you own at the worst hour of the week.
Separate keys also make the last-used data readable. One job calling from one address range is a pattern, and you'll see it when the pattern changes.
Where keys leak in practice, and a ten-minute search
Most leaked keys don't come from break-ins. They come from habits. Someone pastes a failing curl command into a chat channel with the header still attached. A debug logger prints full request headers into a tool that forty people can search. Or a .env file gets committed because the ignore rule landed one commit too late.
You can check for all of these before lunch. Search your log tool for the string zb_live_ and read what comes back. Run the same search across your repository history and not only the current tree, because a deleted file still lives in old commits. Then look at your error tracker, since many of them capture outgoing headers unless you scrub them.
Any hit means the key is spent, even if the channel was private. Rotate it. The prefix exists to make this search possible, so put the search on a calendar and don't wait for a scare. When a 401 with invalid_api_key appears right after a deploy, check the prefix against the environment first.
Questions developers ask
Can I use a key from the browser?
No. A key in front-end code is public the moment the page loads. Put your own endpoint in front of ours and keep the key on the server.
How do I rotate a key with no downtime?
Create the second key first, deploy it, confirm traffic has moved on the key's last-used timestamp, then revoke the old one. Both keys work during the overlap.
Do sandbox keys cost credits?
No. A sandbox key never opens an SMTP connection and never decrements your balance, so you can run it on every commit.
What happens if I send no key?
You get a 401 with authentication_error. The request never reaches the verification queue, so nothing is billed.