Email verification in cURL

curl is the fastest way to find out whether a problem is your code or our API. Every command below runs as written once ZAPBOUNCE_KEY is exported, and each one is the exact request the libraries on the other pages build.

Two flags are worth adding by habit. --fail-with-body makes curl exit non-zero on a 4xx while still printing the error, which is what a shell script needs. -sS keeps the progress bar out of your pipe and leaves errors visible.

curl 7.76 or later for --fail-with-body. Pipe through jq if you want the output readable.

Verify one address

You branch on four results: valid, invalid, catch_all and unknown. Role and disposable are boolean flags beside the result, so test them separately. Unknown is the branch most code forgets, and it is never billed.

Shell
export ZAPBOUNCE_KEY="zb_live_..."

# One address.
curl -sS --fail-with-body https://api.zapbounce.com/v1/verify \
  -H "Authorization: Bearer $ZAPBOUNCE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com"}' | jq

# Just the verdict, for a shell condition.
verdict=$(curl -sS https://api.zapbounce.com/v1/verify \
  -H "Authorization: Bearer $ZAPBOUNCE_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"email\":\"$1\"}" | jq -r '.result')

case "$verdict" in
  valid)     echo "send" ;;
  catch_all) echo "segment separately: the domain accepts everything" ;;
  unknown)   echo "no verdict, nothing billed, try again tomorrow" ;;
  *)         echo "suppress" ;;
esac

Verify a list

Submit a batch rather than looping the single endpoint. We pace probes per receiving mail host, which protects the sending reputation your results depend on.

Shell
# Submit a batch from a file of addresses, one per line.
batch_id=$(jq -R -s -c 'split("\n") | map(select(length > 0))' contacts.txt \
  | jq -c '{name: "cli-run", emails: .}' \
  | curl -sS --fail-with-body https://api.zapbounce.com/v1/batches \
      -H "Authorization: Bearer $ZAPBOUNCE_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d @- | jq -r '.batch_id')

echo "batch $batch_id"

# Poll until it finishes. A webhook is better; this is for a one-off.
until [ "$(curl -sS https://api.zapbounce.com/v1/batches/$batch_id \
            -H "Authorization: Bearer $ZAPBOUNCE_KEY" | jq -r '.status')" = "complete" ]; do
  sleep 10
done

# Page the results through the cursor into one JSONL file.
cursor=""
while :; do
  page=$(curl -sS "https://api.zapbounce.com/v1/batches/$batch_id/results?limit=1000${cursor:+&cursor=$cursor}" \
    -H "Authorization: Bearer $ZAPBOUNCE_KEY")

  echo "$page" | jq -c '.data[]' >> results.jsonl
  [ "$(echo "$page" | jq -r '.has_more')" = "true" ] || break
  cursor=$(echo "$page" | jq -r '.next_cursor')
done

# What came back unresolved, and therefore free.
jq -r 'select(.billed == false) | .email' results.jsonl | wc -l

# A large file goes up as multipart instead.
curl -sS --fail-with-body https://api.zapbounce.com/v1/files \
  -H "Authorization: Bearer $ZAPBOUNCE_KEY" \
  -F "file=@contacts.csv" \
  -F "email_column=work_email" | jq

Errors and retries

A 429 and a 5xx are worth retrying. A 402 for credits and a 400 for a malformed address will fail the same way on every attempt, so stop.

Shell
# --fail-with-body: non-zero exit AND the error body, which -f alone swallows.
if ! response=$(curl -sS --fail-with-body https://api.zapbounce.com/v1/verify \
      -H "Authorization: Bearer $ZAPBOUNCE_KEY" \
      -H "Content-Type: application/json" \
      -d '{"email":"ada@example.com"}'); then

  code=$(echo "$response" | jq -r '.error.code')
  request_id=$(echo "$response" | jq -r '.error.request_id')
  echo "failed: $code (request_id=$request_id)" >&2

  [ "$code" = "insufficient_credits" ] && exit 2   # retrying will not help
  exit 1
fi

# Retry with backoff, honoring Retry-After on a 429.
attempt=0
while [ $attempt -lt 5 ]; do
  http_code=$(curl -sS -o /tmp/zb.json -w '%{http_code}' \
    https://api.zapbounce.com/v1/verify \
    -H "Authorization: Bearer $ZAPBOUNCE_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"email\":\"$1\"}")

  [ "$http_code" = "200" ] && break
  [ "$http_code" = "402" ] && { echo "out of credits" >&2; exit 2; }

  sleep $(( 2 ** attempt ))
  attempt=$(( attempt + 1 ))
done

cat /tmp/zb.json | jq

cURL: common questions

Can I put the key in the URL?

We do not accept it there. Query strings land in access logs, in shell history and in the Referer header, and a header costs nothing.

How do I check my balance?

curl -sS https://api.zapbounce.com/v1/credits -H "Authorization: Bearer $ZAPBOUNCE_KEY" | jq. The ledger comes with it.

Is there a CLI?

Not yet. These commands plus jq cover everything a CLI would wrap, and they work on any machine with curl already installed.

Run this cURL code today

100 free checks a month, no card, credits that never expire, and unknown results that cost nothing.