Email Verification API in cURL, Python, Node.js, PHP, and Go - Copy-Paste Examples | BounceZero
| API & Integration | 9 min read | | 112 views

Email Verification API in cURL, Python, Node.js, PHP, and Go - Copy-Paste Examples

Production-ready code samples for calling an email verification API across five common backend languages. Each example covers single verification, bulk submission, polling for async results, error handling, and rate-limit backoff. Copy, paste, and ship.

Email verification APIs all behave roughly the same way - submit one address or a batch, get back a structured result indicating whether the address is deliverable. The differences live in the small details: how you handle errors, how you back off on rate-limit responses, how you poll long-running bulk jobs without burning quota.

This post is the copy-paste reference: working code for single verification, bulk verification, and async polling in cURL, Python, Node.js, PHP, and Go. The patterns apply to any verification API; we use BounceZero's endpoints in the examples but the same code shape works with ZeroBounce, NeverBounce, Kickbox, or any other provider after swapping the base URL and auth header.

The Standard Request/Response Shape

Most verification APIs converge on the same shape:

Single-address request:

``

POST /v1/verify

Authorization: Bearer YOUR_API_KEY

Content-Type: application/json

{

"email": "[email protected]"

}

`

Single-address response:

`

{

"email": "[email protected]",

"result": "verified", // verified | invalid | catch-all | risky | disposable | role | unknown

"score": 0.97, // 0.0 to 1.0 confidence

"reason": "smtp_accepted", // machine-readable reason code

"did_you_mean": null, // typo suggestion if applicable

"is_disposable": false,

"is_role": false,

"is_free": false,

"is_catchall": false,

"mx_records": ["mail.example.com"],

"verified_at": "2026-06-08T10:30:00Z"

}

`

For BounceZero specifically, the base URL is https://api.bouncezero.io and authentication is Authorization: Bearer YOUR_API_KEY`. Full API reference is at [our docs](https://bouncezero.io/docs).

cURL - Single Verification

Quickest way to test an API key and confirm an endpoint is up:

``bash

curl -X POST https://api.bouncezero.io/v1/verify \

-H "Authorization: Bearer YOUR_API_KEY" \

-H "Content-Type: application/json" \

-d '{"email": "[email protected]"}'

`

With jq for readable output:

`bash

curl -sX POST https://api.bouncezero.io/v1/verify \

-H "Authorization: Bearer YOUR_API_KEY" \

-H "Content-Type: application/json" \

-d '{"email": "[email protected]"}' | jq

`

Processing a text file of emails (one per line):

`bash

while IFS= read -r email; do

curl -sX POST https://api.bouncezero.io/v1/verify \

-H "Authorization: Bearer YOUR_API_KEY" \

-H "Content-Type: application/json" \

-d "{\"email\": \"$email\"}" \

| jq -r '[.email, .result, .score] | @csv'

done < emails.txt

``

For more than a few hundred addresses, switch to the bulk endpoint - it's faster and uses less quota.

Python - Single + Bulk + Backoff

Python with requests (the standard) and basic backoff on rate limits:

``python

import requests

import time

import os

API_KEY = os.environ['BOUNCEZERO_API_KEY']

BASE_URL = 'https://api.bouncezero.io'

HEADERS = {

'Authorization': f'Bearer {API_KEY}',

'Content-Type': 'application/json',

}

def verify(email: str, max_retries: int = 3) -> dict:

"""Single email verification with exponential backoff on 429."""

for attempt in range(max_retries):

r = requests.post(

f'{BASE_URL}/v1/verify',

json={'email': email},

headers=HEADERS,

timeout=10,

)

if r.status_code == 429:

# Rate limited - backoff and retry

wait = int(r.headers.get('Retry-After', 2 attempt))

time.sleep(wait)

continue

r.raise_for_status()

return r.json()

raise RuntimeError(f'Rate limited after {max_retries} retries')

# Usage

result = verify('[email protected]')

print(f"{result['email']}: {result['result']} (score: {result['score']})")

`

Bulk verification with async polling**:

`python

def submit_bulk(emails: list) -> str:

"""Submit a list of emails; returns job_id."""

r = requests.post(

f'{BASE_URL}/v1/bulk',

json={'emails': emails},

headers=HEADERS,

timeout=30,

)

r.raise_for_status()

return r.json()['job_id']

def poll_until_complete(job_id: str, poll_interval: int = 5, max_wait_sec: int = 3600) -> dict:

"""Poll a bulk job until complete; returns final results."""

elapsed = 0

while elapsed < max_wait_sec:

r = requests.get(f'{BASE_URL}/v1/bulk/{job_id}', headers=HEADERS, timeout=10)

r.raise_for_status()

status = r.json()

if status['status'] == 'completed':

return status

if status['status'] == 'failed':

raise RuntimeError(f"Job failed: {status.get('error')}")

time.sleep(poll_interval)

elapsed += poll_interval

raise TimeoutError(f'Job did not complete within {max_wait_sec}s')

# Usage

job_id = submit_bulk(['[email protected]', '[email protected]', 'invalid@nodomain'])

result = poll_until_complete(job_id)

for row in result['results']:

print(f"{row['email']}: {row['result']}")

`

Key rules: respect Retry-After` headers; use exponential backoff; cap retries to avoid runaway; never poll faster than 5-10 seconds for bulk jobs (you waste quota and the server gives the same answer).

Node.js - Async/Await with Native fetch

Modern Node 18+ has native fetch - no need for axios for simple cases:

``javascript

const API_KEY = process.env.BOUNCEZERO_API_KEY;

const BASE_URL = 'https://api.bouncezero.io';

const headers = {

'Authorization': Bearer ${API_KEY},

'Content-Type': 'application/json',

};

async function verify(email, maxRetries = 3) {

for (let attempt = 0; attempt < maxRetries; attempt++) {

const res = await fetch(${BASE_URL}/v1/verify, {

method: 'POST',

headers,

body: JSON.stringify({ email }),

});

if (res.status === 429) {

const wait = parseInt(res.headers.get('Retry-After') || Math.pow(2, attempt), 10);

await new Promise(r => setTimeout(r, wait * 1000));

continue;

}

if (!res.ok) {

throw new Error(API error ${res.status}: ${await res.text()});

}

return await res.json();

}

throw new Error(Rate limited after ${maxRetries} retries);

}

// Usage

(async () => {

const result = await verify('[email protected]');

console.log(${result.email}: ${result.result} (score: ${result.score}));

})();

`

Bulk submission + polling:

`javascript

async function submitBulk(emails) {

const res = await fetch(${BASE_URL}/v1/bulk, {

method: 'POST',

headers,

body: JSON.stringify({ emails }),

});

if (!res.ok) throw new Error(Bulk submit failed: ${res.status});

const data = await res.json();

return data.job_id;

}

async function pollBulk(jobId, intervalMs = 5000, maxWaitMs = 3600000) {

const start = Date.now();

while (Date.now() - start < maxWaitMs) {

const res = await fetch(${BASE_URL}/v1/bulk/${jobId}, { headers });

if (!res.ok) throw new Error(Poll failed: ${res.status});

const status = await res.json();

if (status.status === 'completed') return status;

if (status.status === 'failed') throw new Error(Job failed: ${status.error});

await new Promise(r => setTimeout(r, intervalMs));

}

throw new Error('Timed out waiting for bulk job');

}

`

For production workloads, wrap with proper logging, OpenTelemetry tracing if you have it, and circuit-breaker patterns (e.g., opossum` library) so verifier outages don't cascade into your signup flow.

PHP - Single + Bulk

Pure PHP with no Composer dependencies, using curl_exec:

``php

<?php

$apiKey = getenv('BOUNCEZERO_API_KEY');

$baseUrl = 'https://api.bouncezero.io';

function bzVerify(string $email, string $apiKey, string $baseUrl): array {

$maxRetries = 3;

for ($attempt = 0; $attempt < $maxRetries; $attempt++) {

$ch = curl_init("{$baseUrl}/v1/verify");

curl_setopt_array($ch, [

CURLOPT_POST => true,

CURLOPT_RETURNTRANSFER => true,

CURLOPT_TIMEOUT => 10,

CURLOPT_POSTFIELDS => json_encode(['email' => $email]),

CURLOPT_HTTPHEADER => [

"Authorization: Bearer {$apiKey}",

'Content-Type: application/json',

],

CURLOPT_HEADER => true,

]);

$response = curl_exec($ch);

$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);

$header = substr($response, 0, $headerSize);

$body = substr($response, $headerSize);

curl_close($ch);

if ($code === 429) {

preg_match('/Retry-After:\s*(\d+)/i', $header, $m);

$wait = isset($m[1]) ? (int)$m[1] : pow(2, $attempt);

sleep($wait);

continue;

}

if ($code !== 200) {

throw new Exception("API error {$code}: {$body}");

}

return json_decode($body, true);

}

throw new Exception('Rate limited after retries');

}

// Usage

$result = bzVerify('[email protected]', $apiKey, $baseUrl);

echo "{$result['email']}: {$result['result']} (score: {$result['score']})\n";

`

For Composer-based projects, Guzzle is cleaner:

`php

use GuzzleHttp\Client;

$client = new Client([

'base_uri' => 'https://api.bouncezero.io',

'timeout' => 10,

'headers' => [

'Authorization' => "Bearer {$apiKey}",

'Content-Type' => 'application/json',

],

]);

$response = $client->post('/v1/verify', ['json' => ['email' => '[email protected]']]);

$result = json_decode($response->getBody(), true);

``

Go - Strongly-Typed Client

Idiomatic Go with structs and errors:

``go

package main

import (

"bytes"

"encoding/json"

"errors"

"fmt"

"io"

"math"

"net/http"

"os"

"strconv"

"time"

)

type Client struct {

APIKey string

BaseURL string

HTTP *http.Client

}

type VerifyResult struct {

Email string json:"email"

Result string json:"result"

Score float64 json:"score"

Reason string json:"reason"

DidYouMean string json:"did_you_mean"

IsDisposable bool json:"is_disposable"

IsRole bool json:"is_role"

IsCatchall bool json:"is_catchall"

VerifiedAt string json:"verified_at"

}

func NewClient(apiKey string) *Client {

return &Client{

APIKey: apiKey,

BaseURL: "https://api.bouncezero.io",

HTTP: &http.Client{Timeout: 10 * time.Second},

}

}

func (c *Client) Verify(email string) (*VerifyResult, error) {

body, _ := json.Marshal(map[string]string{"email": email})

for attempt := 0; attempt < 3; attempt++ {

req, _ := http.NewRequest("POST", c.BaseURL+"/v1/verify", bytes.NewReader(body))

req.Header.Set("Authorization", "Bearer "+c.APIKey)

req.Header.Set("Content-Type", "application/json")

resp, err := c.HTTP.Do(req)

if err != nil {

return nil, fmt.Errorf("request failed: %w", err)

}

defer resp.Body.Close()

if resp.StatusCode == 429 {

wait := int(math.Pow(2, float64(attempt)))

if ra := resp.Header.Get("Retry-After"); ra != "" {

if w, err := strconv.Atoi(ra); err == nil {

wait = w

}

}

time.Sleep(time.Duration(wait) * time.Second)

continue

}

if resp.StatusCode != 200 {

msg, _ := io.ReadAll(resp.Body)

return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, msg)

}

var result VerifyResult

if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {

return nil, fmt.Errorf("decode: %w", err)

}

return &result, nil

}

return nil, errors.New("rate limited after retries")

}

func main() {

client := NewClient(os.Getenv("BOUNCEZERO_API_KEY"))

result, err := client.Verify("[email protected]")

if err != nil {

fmt.Fprintf(os.Stderr, "Error: %v\n", err)

os.Exit(1)

}

fmt.Printf("%s: %s (score: %.2f)\n", result.Email, result.Result, result.Score)

}

``

For production use add context.Context support, proper logging, and consider go-resty for the retry/backoff logic if you don't want to write it yourself.

Rate Limits and How to Stay Under Them

Every verification API has rate limits. The shape is usually:

  • Per-second limit (e.g., 50/sec) - prevents burst floods.
  • Per-minute or per-hour limit (e.g., 5000/hour) - overall throughput.
  • Concurrent connection limit (e.g., max 10 in flight) - prevents pool exhaustion.

The right strategy for high-volume:

  • Use bulk endpoints for anything over ~100 addresses. One bulk submit = one rate-limit hit; 1000 single calls = 1000 rate-limit hits.
  • Respect Retry-After headers. When you get a 429, wait the indicated time before retrying.
  • Exponential backoff with jitter - min(maxWait, base * 2^attempt + random()). Jitter prevents thundering herd when multiple clients all hit a 429 at the same moment.
  • Bounded concurrency - limit how many in-flight requests you have. Use a semaphore (Python asyncio.Semaphore, Node p-limit, Go buffered channel).
  • Backpressure your producer - if you're feeding the verifier from a queue, slow the queue consumer based on observed rate-limit responses.
  • Don't retry without backoff. Don't ignore Retry-After. Don't use unbounded concurrency. These are the three patterns that get accounts suspended.

    Error Handling: What Each Status Code Means

    Real-world status codes you'll see from any verification API:

    CodeMeaningAction
    200SuccessUse the result
    400Bad request (e.g., missing email field)Fix the request; do not retry
    401Unauthorized (bad API key)Check your API key; do not retry
    402Payment required (out of credits)Upgrade plan or wait for renewal
    403Forbidden (API key disabled, IP blocked)Contact support
    422Unprocessable (e.g., email syntax invalid)The address is the answer - handle as result: invalid
    429Rate limitedBackoff per Retry-After header
    500/502/503/504Server errorRetry with backoff (capped)

    Critical rule: 4xx errors (except 429) are YOUR problem - don't retry. 5xx errors are temporary - retry with backoff. 429 is rate-limit - wait the indicated time. Confusing 5xx with 4xx (retrying on 401) wastes API calls and triggers more rate-limiting.

    Frequently Asked Questions

    Should I call the verification API client-side or server-side?

    Server-side for any value-of-record decision (account creation, list cleanup, billing). Client-side is fine for UX (real-time form feedback) but always through a proxy endpoint you control, with rate-limiting per-IP. Never put your verification API key in client JavaScript - it's exposed and abusable.

    What's the difference between single and bulk endpoints?

    Single endpoint returns a verdict synchronously in 200-1500ms. Bulk submits N addresses, returns a job_id, and you poll for completion (typically seconds to minutes for thousands of addresses). Use single for real-time flows (signup forms, account creation); use bulk for batch jobs (list cleanup, weekly hygiene, CSV imports).

    How should I handle the 'unknown' result?

    Accept the signup and re-verify asynchronously. 'Unknown' means the verifier couldn't determine - typically due to provider throttling or temporary network issues, not because the address is bad. Blocking on 'unknown' punishes legitimate users for verifier outages. Re-check 6-12 hours later when provider rate limits have reset.

    What HTTP timeout should I use?

    5-10 seconds for single verification. Most calls complete in under 1.5s; a 10s timeout absorbs occasional slow paths (catch-all detection, complex domain probing). For bulk POST submission use 30s. For bulk polling use 5-10s and don't poll more often than every 5 seconds - the answer won't change faster than that.

    Get an API Key - 100 Free Credits

    Same endpoints, same response shape as the examples above. Working API key in 30 seconds, no credit card required.

    Get Started Free
    email verification api code examples python node.js php go curl
    AL

    Written by

    Ayoub Lebda

    Founder, BounceZero - Email-infrastructure engineer

    Ayoub built BounceZero's 5-stage validation pipeline, its dedicated BGP-announced IP infrastructure, and the Patroni HA PostgreSQL cluster behind every verification. Previously built high-volume email delivery infrastructure. Trained at 1337 Benguerir (École 42 network, 2019). Open-source: bgp_analyzer.