API documentation

REST · JSON · Bearer auth. Email, IP, phone and email batches, with Node.js and Python SDKs.

Data validation: results for each check

Request result_format=validation-v1 for checks and attributes for each field, without scores or an acceptance decision. The client decides how to use each result.

curl -X POST "https://api.byebouncer.com/api/v1/lead/verify?result_format=validation-v1" \
  -H "Authorization: Bearer $BB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"person@example.com"}'

Illustrative response; example.com is not offered as a real test mailbox.

{
  "result_format": "validation-v1",
  "execution_status": "completed",
  "results": {
    "email": {
      "input": "person@example.com",
      "deliverability": "deliverable",
      "checks": {
        "syntax": {
          "state": "completed",
          "value": true
        },
        "smtp": {
          "state": "completed",
          "value": "recipient_accepted"
        }
      },
      "attributes": {
        "name": "example.com"
      }
    }
  }
}
  • Email: deliverability, server response and attributes such as catch-all or role account.
  • Phone: format, normalized number, carrier and line type. Mobile network lookup is optional and has an additional cost.
  • IP: scope, network information and each queried source’s result. VPN/proxy lookup is not included.

Execution states: completed, not_requested, not_applicable, unavailable and failed. A completed check can have a negative finding; an unavailable service does not mean the data is invalid. An HTTP 200 response can be partial.

Validation does not establish identity, ownership, consent or commercial intent. SMTP acceptance does not guarantee delivery or reading. Valid format or an identified carrier does not establish line connectivity.

Endpoints and billing

/api/v1/verify, /api/v1/verify-phone, /api/v1/verify-phone/live, /api/v1/verify-ip, /api/v1/lead/verify: ?result_format=validation-v1.

Only submitted fields are checked. Unit prices and access controls remain unchanged. In this format, digit patterns and VoIP do not block carrier checks on quality grounds; a lookup previously skipped may now be billable. Errors, inapplicable checks and debits follow each service’s policy.

List jobs retain the format selected at creation; their CSV includes checks and attributes. Existing jobs and integrations retain their original contract. Public bulk support remains email-only; this change does not add multi-field CSV uploads.

SDK: new methods

In SDK release candidate 0.3.0, pending coordinated publication: Node.js: validate, validateEmail, validatePhone, lookupIP. Python: validate, validate_email, validate_phone, lookup_ip (sync and async clients).

Legacy API compatibility

Without result_format, the v1 response is preserved, including legacy fields. Do not interpret legacy scores as fraud probability or authenticity. Migrate by reading results and checks in the new format. No removal date is scheduled; it will be announced after reviewing active consumers.

OpenAPI

v1 compatibility reference (legacy format)

Quickstart

Create your first bb_live_... API key in the dashboard. Each account receives 500 welcome credits once. Store your secret key in the BB_API_KEY environment variable on your server. Base URL:

https://api.byebouncer.com
curl --fail-with-body --max-time 30 -X POST https://api.byebouncer.com/api/v1/verify \
  -H "Authorization: Bearer $BB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"someone@example.com"}'

The JavaScript examples run on Node.js 18 or later; save them in an .mjs file. The Python REST examples use httpx (pip install httpx). To use the official clients, follow the SDK installation instructions.

Official SDKs: Node.js and Python

Node.js 18+ supports ESM and CommonJS. Python 3.9+ provides synchronous and asynchronous clients. Install or update the package for your language:

npm install @bye_bouncer/sdk@latest

Public packages: @bye_bouncer/sdk on npm and byebouncer-sdk on PyPI. In Python, install byebouncer-sdk and import byebouncer. Both clients use the production URL by default.

Your first SDK verification

Complete server examples using BB_API_KEY:

import { ByeBouncer } from "@bye_bouncer/sdk";

const client = new ByeBouncer({
  apiKey: process.env.BB_API_KEY,
  timeoutMs: 30_000,
});
const result = await client.verify("someone@example.com");
console.log(result.status, result.action, result.credits_remaining);

Node.js with CommonJS

If your project uses require, save this example in a .cjs file:

const { ByeBouncer } = require("@bye_bouncer/sdk");

async function main() {
  const client = new ByeBouncer({ apiKey: process.env.BB_API_KEY });
  const result = await client.verify("someone@example.com");
  console.log(result.status, result.action);
}
main().catch((error) => {
  console.error(error.message);
  process.exitCode = 1;
});

Asynchronous Python

import asyncio
import os
from byebouncer import AsyncByeBouncer

async def main():
    async with AsyncByeBouncer(api_key=os.environ["BB_API_KEY"], timeout=30) as client:
        result = await client.verify("someone@example.com")
        print(result.status, result.action)

asyncio.run(main())

Available methods

OperationNode.jsPythonEndpoint
Emailverify(email)verify(email)POST /api/v1/verify
IPverifyIP(ip)verify_ip(ip)POST /api/v1/verify-ip
PhoneverifyPhone(phone, opts)verify_phone(phone, ...)POST /api/v1/verify-phone
Balancecredits()credits()GET /api/v1/credits
Create batchbulk(opts, idempotencyKey)bulk(emails, idempotency_key=...)POST /api/v1/bulk
Get batchbulkStatus(id)bulk_status(id)GET /api/v1/bulk/{id}
Cancel batchbulkCancel(id)bulk_cancel(id)DELETE /api/v1/bulk/{id}
Download CSVbulkDownloadUrl(id)bulk_download_url(id)GET /api/v1/bulk/{id}/download
Batch webhookbulkSetWebhook(id, opts)bulk_set_webhook(id, ...)POST /api/v1/bulk/{id}/webhook
Blocklist: addaddPhoneBlacklist(opts)add_phone_blacklist(phone, ...)POST /api/v1/phone/blacklist
Blocklist: removeremovePhoneBlacklist(phone)remove_phone_blacklist(phone)DELETE /api/v1/phone/blacklist

The asynchronous Python clients expose the same methods with await. For combined leads, HLR (live: true), suggestions and domains, use the REST examples on this page: the current SDKs do not have dedicated methods for these operations. IP and phone bulk methods have been retired and fail before making a request.

Errors, timeouts and retries

HTTP errors throw ByeBouncerError with status, code and detail. HTTP 401 uses UnauthorizedError, 402 uses InsufficientCreditsError and 429 uses RateLimitError. Read the suggested wait time in retryAfterSeconds (Node) or retry_after_seconds (Python).

Both clients use a 30-second timeout by default. Node applies timeoutMs until the entire response is read, including CSV downloads, and propagates AbortError when it expires. Python uses timeout for network operations and propagates httpx timeout errors. If you supply your own Python HTTP client, configure its timeout and close it yourself.

There are no automatic retries: a verification that timed out may already have consumed credits. Every billable check costs credits, including cached results. To retry batch creation, keep the same idempotency key and payload; use a new key for a different import.

Authentication

All endpoints that consume credits require the Authorization: Bearer <api_key> header.

  • Live keys (bb_live_*): backend use, no domain restrictions, all endpoints.
  • Public keys (bb_pub_*): frontend use (JS widget), with domain-restricted CORS. Allow /verify, /verify-ip, /verify-phone and /lead/verify when enabled. /suggest is public and needs no key. Each public key needs an explicit credit cap. Allowed domains control browsers; they do not prove the identity of other HTTP clients.

Never commit API keys to your repository. Rotate them in the dashboard if you suspect a compromise. Use live keys and the SDKs in this guide on your server. Balance, bulk, phone blocklist and HLR operations require a live key.

POST /api/v1/verify

POSTAuth: Live/Public1 credit

Verifies an email address. Each check costs 1 credit, including responses served from our internal cache.

If our temporary per-domain limit prevents an SMTP lookup, we return 429 smtp_domain_rate_limited with Retry-After: 60 and charge no credits. Wait before retrying; use bulk for lists with many addresses at the same domain. An actual unknown result, such as a catch-all, follows the normal billing policy.

SMTP verification is a signal, not a delivery guarantee. Gmail and other providers may accept or reject recipients according to their anti-abuse policies. Treat unknown as requiring manual review, rather than as valid.

Request

{
  "email": "someone@example.com"
}

Response (200)

{
  "email": "someone@example.com",
  "status": "deliverable",
  "action": "allow",
  "flagged": false,
  "signals": [
    "valid_syntax",
    "mx_found",
    "mailbox_accepts_mail"
  ],
  "domain": {
    "name": "example.com",
    "type": "business",
    "disposable": false,
    "free_provider": false,
    "has_mx": true
  },
  "cached": false,
  "response_time_ms": 1230,
  "credits_remaining": 499,
  "verified_at": "2026-08-18T15:30:00Z"
}
curl --fail-with-body --max-time 30 -X POST https://api.byebouncer.com/api/v1/verify \
  -H "Authorization: Bearer $BB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"someone@example.com"}'

POST /api/v1/verify-ip

POSTAuth: Live/Public1 credit per publicly routable IP

Analyzes IPv4 and IPv6. Returns network classification, reputation, signals, a score and a low, medium, high or critical risk level. Private, loopback and other non-routable ranges return status: invalid_scope and consume no credits. Each billable public IP check costs 1 credit. Available when IP validation is enabled.

curl --fail-with-body --max-time 30 -X POST https://api.byebouncer.com/api/v1/verify-ip \
  -H "Authorization: Bearer $BB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ip":"8.8.8.8"}'

POST /api/v1/verify-phone

POSTAuth: Live/Public1 credit locally or 3 with carrier data

Validates format, country and line type using local metadata. For national numbers, send default_country as a two-letter ISO code. Results with invalid, possible or unknown status, and numbers on your blocklist, consume no credits. Each billable result costs 1 credit for local validation or 3 with carrier data, including cached results. Public bulk verification is available for email only. A number on your blocklist returns verdict: no and the blacklisted flag.

valid confirms numbering plan rules. It does not confirm that the line exists, is active or uses SMS or WhatsApp. carrier comes from local metadata and may not reflect number portability. The live line check (HLR) is optional: it only runs when you send live: true (see below), and has a separate cost.

Request

{
  "phone": "11 5555-1234",
  "default_country": "AR"
}

Response (200)

{
  "phone_hash": "a1b2c3…",
  "normalized_e164": "+5491155551234",
  "is_valid": true,
  "status": "valid",
  "country": "AR",
  "country_calling_code": 54,
  "line_type": "mobile",
  "carrier": "Movistar",
  "signals": [
    "country_detected",
    "country_from_default",
    "valid_format",
    "e164_normalized",
    "mobile_line"
  ],
  "level": "l1",
  "cached": false,
  "credits_remaining": 499,
  "response_time_ms": 1,
  "verified_at": "2026-09-13T15:30:00Z"
}
curl --fail-with-body --max-time 30 -X POST https://api.byebouncer.com/api/v1/verify-phone \
  -H "Authorization: Bearer $BB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"11 5555-1234","default_country":"AR"}'

Live phone check (HLR)

POSTAuth: Live6 credits per conclusive check (optional)

Asks the carrier network whether the line was connected at the last check, to help decide whether to call a lead now. The phone does not ring and the owner is not notified. It takes a few seconds: call it from your backend once you have the lead, rather than while someone is typing in a form.

There are two ways to request it: add "live": true to /api/v1/verify-phone or /api/v1/lead/verify to include it in the normal result, or call POST /api/v1/verify-phone/live for the live status alone. A live API key is required and the service must be enabled.

Request

{
  "phone": "+5491163083418",
  "live": true
}

Additional response fields

Example with 500 starting credits, carrier data (3) and HLR (6): 491 remain.

{
  "live": {
    "status": "connected",
    "connected": true,
    "ported": true,
    "roaming": false,
    "current_network": "Claro Argentina (AMX Argentina S.A.)",
    "origin_network": "Movistar (Telefonica Moviles Argentina S.A.)"
  },
  "contactability": "yes",
  "live_credits_charged": 6,
  "live_cached": false,
  "live_checked_at": "2026-09-29T15:00:00Z",
  "credits_remaining": 491
}

contactability

  • yes — connected at the last network check: you can try calling.
  • retry_later — the phone is off or out of coverage. It is a real line: try again later.
  • unverifiable — landline or VoIP: live status is only available for mobile lines.
  • no — invalid number, unknown to the network or on your blocklist.

“Connected” means registered on the network at the time in live_checked_at. The result may be cached for a few minutes. It does not guarantee that someone will answer or that the number belongs to that person.

Billing

Only conclusive network responses (connected, absent or unknown) are billed. There is no charge for invalid, landline, VoIP or blocklisted numbers, or when the carrier does not respond. Conclusive cached responses cost the same without querying the provider again. If the live check fails, the normal result is still returned with live_error, and only the live part is not charged.

Possible live_error values: live_key_required, phone_live_unavailable, insufficient_credits, hlr_unavailable, hlr_busy and hlr_daily_limit.

POST /api/v1/lead/verify

POSTAuth: Live/PublicSum of billable channels

Verifies lead data in one call. Send at least one of email, ip or phone; each channel is billed according to its result and price. The response includes a combined verdict. For HLR, add live: true and use a live key. Call this endpoint through REST; the current SDKs do not expose verifyLead or verify_lead methods.

scoring.version identifies the contactability-v2 rules. The score combines email and phone with relative weights 40/35; scoring.based_onidentifies contributing channels. IP remains network context and does not affect the overall score or verdict. Country matches and mismatches do not change the phone score. One favorable contact channel is enough to accept, even if the other was rejected; individual results and reasons preserve those issues. Without a favorable channel, pending evidence requires review and negative results can cause rejection. A neutral technical email failure does not score; email_unverified retains the warning. With no scorable contact evidence, scoring.overall is null. The score is not a probability or proof that both channels work.

curl --fail-with-body --max-time 30 -X POST https://api.byebouncer.com/api/v1/lead/verify \
  -H "Authorization: Bearer $BB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"someone@example.com","ip":"8.8.8.8","phone":"+34612345678"}'

Email batches

POSTAuth: Live1 credit reserved per unique email

POST /api/v1/bulk creates an asynchronous job with up to 5,000 unique email addresses per batch. Only email bulk verification is offered; IP and phone bulk routes have been retired. Creation returns job.id and reserves credits. Cancellation refunds unprocessed items; if a job ends with failed, the full debit is refunded.

curl --fail-with-body --max-time 30 -X POST https://api.byebouncer.com/api/v1/bulk \
  -H "Authorization: Bearer $BB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails":["a@example.com","b@example.com"],"filename":"contacts.csv"}'

Create, wait and download with the SDK

This example keeps an import ID for retries and only downloads completed jobs. Choose a unique ID for each import and store it before sending the request. Download the CSV from its signed URL: do not send your API key to that URL.

import { ByeBouncer } from "@bye_bouncer/sdk";

const client = new ByeBouncer({ apiKey: process.env.BB_API_KEY });
const created = await client.bulk(
  { emails: ["a@example.com", "b@example.com"], filename: "contacts.csv" },
  "contacts-2026-10-24", // Unique import ID: save it for retries.
);
const job = await client.bulkWaitForCompletion(created.job.id, {
  intervalMs: 3000,
  timeoutMs: 120_000,
});
if (job.status !== "completed" || !job.result_available) {
  throw new Error("The batch ended without results: " + job.status);
}
const download = await client.bulkDownloadUrl(job.id);
const response = await fetch(download.url, { signal: AbortSignal.timeout(30_000) });
if (!response.ok) throw new Error("HTTP " + response.status);
const csv = await response.text();
console.log(csv);

bulkAndWait and bulk_and_wait also create, wait and download in one call; these helpers do not accept an idempotency key. bulkWaitForCompletion / bulk_wait_for_completion return the terminal state, which can be failed, cancelled or expired as well as completed.

Prefer webhooks for long-running jobs. When sending webhook_url, save the webhook_secret returned at creation: it is not shown again when you retrieve the job. Validate X-ByeBouncer-Signature using HMAC-SHA256 over the original request body before processing the notification.

POST /api/v1/suggest

POSTAuth: PublicFree (20/min per IP)

Detects domain typos and returns a suggestion when the Levenshtein distance from a known domain is ≤ 2. Does not perform a network lookup.

curl --fail-with-body --max-time 30 -X POST https://api.byebouncer.com/api/v1/suggest \
  -H "Content-Type: application/json" \
  -d '{"email":"user@gnail.com"}'

GET /api/v1/domain/:domain

GETAuth: PublicFree (30/min per IP)

Classifies a domain as disposable, free, business or education and performs an MX lookup. Useful for a quick check before accepting a signup.

curl --fail-with-body --max-time 30 -X GET https://api.byebouncer.com/api/v1/domain/mailinator.com

GET /api/v1/credits

GETAuth: LiveFree

credits is the spendable balance for the key, taking its cap into account. wallet_balance, when present, is the shared account balance. They are views of the same balance and must not be added together. This check is free and always reads from the database.

curl --fail-with-body --max-time 30 -X GET https://api.byebouncer.com/api/v1/credits \
  -H "Authorization: Bearer $BB_API_KEY"

GET /api/v1/status

GETAuth: PublicFree

Service status. Returns ok or degraded based on Postgres and Redis connectivity. You can use it with Better Uptime or another external monitor.

For a lightweight check, use GET /api/v1/health without authentication. Returns ok text with HTTP 200 or a degraded state with HTTP 503.

Error codes

HTTPerrorMeaning
400invalid_requestMalformed or empty body
401missing_authorization_headerMissing Bearer header
401invalid_api_keyUnknown or revoked key
402insufficient_creditsInsufficient balance
402credit_cap_not_setSet a credit cap for the public key in the dashboard
403billing_restrictedAccount restricted due to a payment dispute; contact support
403origin_not_allowedPublic key: Origin is not in allowed_domains
403live_key_requiredThis operation requires a live key
429rate_limit_exceededIP or API key rate limit reached
500verify_failedInternal verification error
503pack_not_configuredMissing Paddle price_id environment variable
503authentication_busyAuthentication busy; retry according to Retry-After

Rate limits

Rate limiting uses a sliding window in Redis. Rate limiter responses include:

X-RateLimit-Limit: 50
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1723644800
EndpointLimitWindowIdentifier
Authenticated routes (before resolving Authorization)6,0001 minuteSource IP
Public keys (all endpoints)1,00024 hoursapi_key_id
Public keys (all endpoints)601 minuteVisitor IP
/verify (live key)501 secondapi_key_id
/verify (live key)10,0001 hourapi_key_id
/verify (live key)100,00024 hoursapi_key_id
/verify (public key)101 secondapi_key_id
/verify-ip (live key)50 / 10,0001 second / 1 hourapi_key_id
/verify-ip (public key)10 / 1,0001 second / 1 hourapi_key_id
/verify-phone (live key)501 secondapi_key_id
/verify-phone (live key)10,0001 hourapi_key_id
/verify-phone (public key)101 secondapi_key_id
/verify-phone/live (live key)51 secondapi_key_id
/verify-phone/live (live key)1,0001 hourapi_key_id
/lead/verify (live key)20 / 10,0001 second / 1 hourapi_key_id
/lead/verify (public key)5 / 1,0001 second / 1 hourapi_key_id
POST /bulk (live key)101 minuteapi_key_id
GET /bulk/:id (live key)1201 minuteapi_key_id
DELETE /bulk/:id (live key)201 minuteapi_key_id
GET /bulk/:id/download (live key)301 minuteapi_key_id
POST /bulk/:id/webhook (live key)201 minuteapi_key_id
/suggest201 minuteIP
/domain/:d301 minuteIP
/demo/verify51 hourIP
/demo/verify2024 hoursIP
/status601 minuteIP
/health3001 minuteIP

If Redis is unavailable, verifications and tools that access the network return HTTP 503 without starting that work.