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.
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.comcurl --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@latestPublic 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
| Operation | Node.js | Python | Endpoint |
|---|---|---|---|
| verify(email) | verify(email) | POST /api/v1/verify | |
| IP | verifyIP(ip) | verify_ip(ip) | POST /api/v1/verify-ip |
| Phone | verifyPhone(phone, opts) | verify_phone(phone, ...) | POST /api/v1/verify-phone |
| Balance | credits() | credits() | GET /api/v1/credits |
| Create batch | bulk(opts, idempotencyKey) | bulk(emails, idempotency_key=...) | POST /api/v1/bulk |
| Get batch | bulkStatus(id) | bulk_status(id) | GET /api/v1/bulk/{id} |
| Cancel batch | bulkCancel(id) | bulk_cancel(id) | DELETE /api/v1/bulk/{id} |
| Download CSV | bulkDownloadUrl(id) | bulk_download_url(id) | GET /api/v1/bulk/{id}/download |
| Batch webhook | bulkSetWebhook(id, opts) | bulk_set_webhook(id, ...) | POST /api/v1/bulk/{id}/webhook |
| Blocklist: add | addPhoneBlacklist(opts) | add_phone_blacklist(phone, ...) | POST /api/v1/phone/blacklist |
| Blocklist: remove | removePhoneBlacklist(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-phoneand/lead/verifywhen enabled./suggestis 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
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
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
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)
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
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
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
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
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.comGET /api/v1/credits
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
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
| HTTP | error | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed or empty body |
| 401 | missing_authorization_header | Missing Bearer header |
| 401 | invalid_api_key | Unknown or revoked key |
| 402 | insufficient_credits | Insufficient balance |
| 402 | credit_cap_not_set | Set a credit cap for the public key in the dashboard |
| 403 | billing_restricted | Account restricted due to a payment dispute; contact support |
| 403 | origin_not_allowed | Public key: Origin is not in allowed_domains |
| 403 | live_key_required | This operation requires a live key |
| 429 | rate_limit_exceeded | IP or API key rate limit reached |
| 500 | verify_failed | Internal verification error |
| 503 | pack_not_configured | Missing Paddle price_id environment variable |
| 503 | authentication_busy | Authentication 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| Endpoint | Limit | Window | Identifier |
|---|---|---|---|
| Authenticated routes (before resolving Authorization) | 6,000 | 1 minute | Source IP |
| Public keys (all endpoints) | 1,000 | 24 hours | api_key_id |
| Public keys (all endpoints) | 60 | 1 minute | Visitor IP |
| /verify (live key) | 50 | 1 second | api_key_id |
| /verify (live key) | 10,000 | 1 hour | api_key_id |
| /verify (live key) | 100,000 | 24 hours | api_key_id |
| /verify (public key) | 10 | 1 second | api_key_id |
| /verify-ip (live key) | 50 / 10,000 | 1 second / 1 hour | api_key_id |
| /verify-ip (public key) | 10 / 1,000 | 1 second / 1 hour | api_key_id |
| /verify-phone (live key) | 50 | 1 second | api_key_id |
| /verify-phone (live key) | 10,000 | 1 hour | api_key_id |
| /verify-phone (public key) | 10 | 1 second | api_key_id |
| /verify-phone/live (live key) | 5 | 1 second | api_key_id |
| /verify-phone/live (live key) | 1,000 | 1 hour | api_key_id |
| /lead/verify (live key) | 20 / 10,000 | 1 second / 1 hour | api_key_id |
| /lead/verify (public key) | 5 / 1,000 | 1 second / 1 hour | api_key_id |
| POST /bulk (live key) | 10 | 1 minute | api_key_id |
| GET /bulk/:id (live key) | 120 | 1 minute | api_key_id |
| DELETE /bulk/:id (live key) | 20 | 1 minute | api_key_id |
| GET /bulk/:id/download (live key) | 30 | 1 minute | api_key_id |
| POST /bulk/:id/webhook (live key) | 20 | 1 minute | api_key_id |
| /suggest | 20 | 1 minute | IP |
| /domain/:d | 30 | 1 minute | IP |
| /demo/verify | 5 | 1 hour | IP |
| /demo/verify | 20 | 24 hours | IP |
| /status | 60 | 1 minute | IP |
| /health | 300 | 1 minute | IP |
If Redis is unavailable, verifications and tools that access the network return HTTP 503 without starting that work.