API Reference
Complete BillionVerify API reference for authentication, email verification, file jobs, downloads, credits, and webhooks.
Watch how it works
How to Verify an Email List With the Email Verification API
How to verify an email list with the email verification API: upload a CSV to /verify/file, track the job by task ID, and download the results as CSV, filtered or in full. Then replace polling with a webhook: register an HTTPS URL for file.completed and file.failed, and check each delivery's signature with your secret.
The BillionVerify API is a RESTful service for real-time email verification, synchronous batch checks, asynchronous file processing, credits, and webhooks. All requests must be sent over HTTPS.
Base URL
https://api.billionverify.com/v1OpenAPI Specification
The canonical machine-readable contract is available here:
- OpenAPI YAML: https://api.billionverify.com/openapi.yaml
- Interactive API Docs: https://api.billionverify.com/docs
Authentication
Send your API key in the BV-API-KEY header:
BV-API-KEY: sk_...The following header names are all accepted, in priority order:
BV-API-KEY(recommended)BILLIONVERIFY-API-KEYAuthorization: Bearer <api_key>
Get your API key from the BillionVerify dashboard.
Example
curl -X POST https://api.billionverify.com/v1/verify/single \
-H "BV-API-KEY: sk_xxx" \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","check_smtp":false}'Rate Limits
Rate limits are applied per API key and endpoint type.
| Endpoint | Rate Limit |
|---|---|
Single Verification (/verify/single) | 6,000 requests/minute |
Batch Verification (/verify/bulk) | 1,500 requests/minute |
File Upload (/verify/file) | 300 requests/minute |
Disposable Check (/verify/disposable) | 6,000 requests/minute |
Other endpoints (/credits, /webhooks, etc.) | 600 requests/minute |
When rate limited, the API returns 429 Too Many Requests:
{
"success": false,
"code": "4290",
"message": "Rate limit exceeded",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please try again later."
}
}Response Format
Successful responses:
{
"success": true,
"code": "0",
"message": "Success",
"data": {}
}Error responses:
{
"success": false,
"code": "4010",
"message": "Unauthorized",
"error": {
"code": "INVALID_API_KEY",
"message": "API key is invalid or missing"
}
}Endpoints
Single Email Verification
Verify a single email synchronously.
POST /verify/singleRequest
{
"email": "user@example.com",
"check_smtp": false
}Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
email | string | Yes | - | Email address to verify |
check_smtp | boolean | No | true | Perform live SMTP mailbox verification |
force_refresh | boolean | No | false | Bypass cached results and force a fresh live verification |
Notes
check_smtpdefaults totruewhen omitted.- When
check_smtp=false, the request skips live SMTP network checks. force_refreshdefaults tofalse. Set totrueonly when you need the most current result and are willing to consume a credit even if a cached result exists.
Response
{
"success": true,
"code": "0",
"message": "Success",
"data": {
"email": "user@example.com",
"status": "valid",
"score": 0.95,
"is_deliverable": true,
"is_disposable": false,
"is_catchall": false,
"is_role": false,
"is_free": false,
"has_gravatar": false,
"gravatar_url": "",
"domain": "example.com",
"domain_age": 10,
"mx_records": ["mail.example.com"],
"domain_reputation": {
"mx_ip": "192.0.2.1",
"is_listed": false,
"blacklists": [],
"checked": true
},
"smtp_check": false,
"reason": "accepted",
"smtp_response": "",
"error_message": "",
"domain_suggestion": "",
"response_time": 250,
"credits_used": 1
}
}Batch Email Verification
Verify multiple email addresses synchronously. Use this for small batches of up to 50 emails.
POST /verify/bulkRequest
{
"emails": ["user1@example.com", "user2@example.com"],
"check_smtp": false
}Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
emails | array | Yes | - | Array of email addresses (max: 50) |
check_smtp | boolean | No | true | Perform live SMTP mailbox verification |
Response
{
"success": true,
"code": "0",
"message": "Success",
"data": {
"results": [
{
"email": "user1@example.com",
"status": "valid",
"score": 0.95,
"is_deliverable": true,
"credits_used": 1
},
{
"email": "user2@example.com",
"status": "invalid",
"score": 0,
"is_deliverable": false,
"credits_used": 0
}
],
"total_emails": 2,
"valid_emails": 1,
"invalid_emails": 1,
"credits_used": 1,
"process_time": 1500
}
}Disposable Email Check
Check whether an email address belongs to a known disposable (temporary) email provider. This endpoint is free — it performs an in-memory domain lookup and does not consume any credits.
POST /verify/disposable
GET /verify/disposable?email=user@example.comRequest (POST)
{
"email": "user@example.com"
}Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address to check |
Notes
- This endpoint does not charge credits.
check_smtpis not supported. For full SMTP verification use/verify/single,/verify/bulk, or/verify/filewithcheck_smtp: true.- Accepts both
POSTwith a JSON body andGETwith an?email=query parameter.
Response
{
"success": true,
"code": "0",
"message": "Success",
"data": {
"email": "user@mailnull.com",
"domain": "mailnull.com",
"is_disposable": true,
"checked_at": "2026-05-19T10:00:00Z"
}
}Response Fields
| Field | Type | Description |
|---|---|---|
email | string | Normalised email address (lowercase) |
domain | string | Domain portion of the email (ASCII / Punycode) |
is_disposable | boolean | true if the domain is a known disposable provider |
checked_at | string | UTC timestamp of the check (RFC 3339) |
File Upload Verification
Upload a CSV, TXT, or XLSX file for asynchronous verification.
POST /verify/fileMultipart Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
file | file | Yes | - | CSV, TXT, or XLSX file |
check_smtp | boolean | No | true | Perform live SMTP mailbox verification |
email_column | string | No | auto-detect | Column containing email addresses |
preserve_original | boolean | No | true | Keep original columns in the result file |
Limits
No application-enforced file-size or row-count limit.
Response
{
"success": true,
"code": "0",
"message": "Success",
"data": {
"task_id": "7143874e-21c5-43c1-96f3-2b52ea41ae6a",
"file_name": "emails.csv",
"file_size": 45678,
"status": "pending",
"message": "File uploaded successfully, parsing and processing email verification",
"status_url": "https://api.billionverify.com/v1/verify/file/7143874e-21c5-43c1-96f3-2b52ea41ae6a",
"created_at": "2026-02-04T10:30:00Z",
"estimated_count": 1000,
"unique_emails": 950,
"total_rows": 1000,
"email_column": "email"
}
}Get File Job Status
Get the latest status for a file job.
GET /verify/file/{job_id}Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
timeout | integer | No | 0 | Long-polling timeout in seconds (0-300) |
Download Link Semantics
download_urlis the stable API entrypoint for result downloads.direct_download_urlis a signed storage URL for immediate download.direct_download_expires_attells you when that signed storage URL expires.result_file_urlandresult_file_pathare not part of the public API contract.
Response
{
"success": true,
"code": "0",
"message": "Success",
"data": {
"task_id": "7143874e-21c5-43c1-96f3-2b52ea41ae6a",
"status": "completed",
"progress": 100,
"email_progress": 100,
"chunk_progress": 100,
"progress_source": "email",
"total_emails": 1000,
"processed_emails": 1000,
"valid_emails": 850,
"invalid_emails": 100,
"unknown_emails": 30,
"role_emails": 15,
"catchall_emails": 5,
"risky_emails": 0,
"disposable_emails": 0,
"credits_used": 970,
"download_url": "https://api.billionverify.com/v1/verify/file/7143874e-21c5-43c1-96f3-2b52ea41ae6a/results",
"direct_download_url": "https://download.example.com/results_7143874e.csv?signature=...",
"direct_download_expires_at": "2026-02-04T11:32:05Z",
"started_at": "2026-02-04T10:30:00Z",
"completed_at": "2026-02-04T10:32:05Z",
"total_chunks": 10,
"completed_chunks": 10,
"failed_chunks": 0,
"unique_emails": 950,
"total_rows": 1000
}
}Job Status Values
| Status | Description |
|---|---|
preparing | File is being parsed and staged |
pending | Job is queued for execution |
running | Workflow has started |
processing | Emails are actively being verified |
merging | Results are being finalized |
paused | Job is paused and can be resumed |
completed | Job finished successfully |
completed_with_warning | Job finished, but with warnings |
failed | Job failed |
cancelled | Job was cancelled |
Download File Results
Download verification results for a completed file job.
GET /verify/file/{job_id}/resultsBehavior
- Without filters, the endpoint responds with
307 Temporary Redirectto the full result file. - With filters, the endpoint returns a generated CSV directly.
- Filters are combined with OR semantics.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
valid | boolean | Include valid emails |
invalid | boolean | Include invalid emails |
risky | boolean | Include risky emails |
unknown | boolean | Include unknown emails |
catchall | boolean | Include catch-all emails |
role | boolean | Include role emails |
disposable | boolean | Include disposable emails |
Example
curl -L -o filtered-results.csv \
"https://api.billionverify.com/v1/verify/file/{job_id}/results?valid=true&catchall=true&role=true" \
-H "BILLIONVERIFY-API-KEY: sk_xxx"Credits
Get the current credit balance for the authenticated account.
GET /creditsResponse
{
"success": true,
"code": "0",
"message": "Success",
"data": {
"account_id": "abc123",
"api_key_id": "key_xyz",
"api_key_name": "Default API Key",
"credits_balance": 9500,
"credits_consumed": 500,
"credits_added": 10000,
"last_updated": "2026-02-04T10:30:00Z"
}
}Webhooks
Use webhooks to receive asynchronous file-job notifications.
Endpoints
POST /webhooksGET /webhooksDELETE /webhooks/{webhook_id}
Supported Events
file.completedfile.failed
verification.completed is not part of the public webhook contract.
Create Webhook
POST /webhooksRequest
{
"url": "https://your-app.com/webhooks/billionverify",
"events": ["file.completed", "file.failed"]
}Rules
urlmust be HTTPSeventsmay contain only supported event names
Response
{
"success": true,
"code": "0",
"message": "Success",
"data": {
"id": "673f3bff-81ea-4730-9cef-af1eb7f134bf",
"url": "https://your-app.com/webhooks/billionverify",
"events": ["file.completed", "file.failed"],
"secret": "5c609e6893ab3b65ce8940549a030bc165762fe171e0b38f880bd570099619bf",
"is_active": true,
"created_at": "2026-02-04T10:30:00Z",
"updated_at": "2026-02-04T10:30:00Z"
}
}The webhook secret is returned only on creation. Store it securely. You need
it to verify webhook signatures.
List Webhooks
GET /webhooksResponse
{
"success": true,
"code": "0",
"message": "Success",
"data": {
"webhooks": [
{
"id": "673f3bff-81ea-4730-9cef-af1eb7f134bf",
"url": "https://your-app.com/webhooks/billionverify",
"events": ["file.completed", "file.failed"],
"is_active": true,
"last_delivery_status": "success",
"last_delivery_at": "2026-04-01T12:00:00Z",
"last_error": null,
"created_at": "2026-02-04T10:30:00Z",
"updated_at": "2026-02-04T10:30:00Z"
}
],
"total": 1
}
}last_delivery_statusissuccessorfailedwhen a delivery has been attempted.last_delivery_atis the timestamp of the most recent final delivery outcome.last_errorcontains the latest final error message, ornullwhen the latest delivery succeeded.- For webhooks that have never been delivered, all three fields are
null.
Delete Webhook
DELETE /webhooks/{webhook_id}Response
{
"success": true,
"code": "0",
"message": "Success",
"data": {
"message": "Webhook deleted successfully",
"webhook_id": "673f3bff-81ea-4730-9cef-af1eb7f134bf"
}
}Webhook Delivery Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Event | Event type |
X-Webhook-Timestamp | Timestamp used in signature verification |
X-Webhook-Signature | sha256=<hex_digest> |
Webhook Payload
{
"event": "file.completed",
"timestamp": "2026-02-04T10:35:00Z",
"data": {
"job_id": "7143874e-21c5-43c1-96f3-2b52ea41ae6a",
"file_name": "emails.csv",
"total_emails": 1000,
"valid_emails": 850,
"invalid_emails": 100,
"role_emails": 30,
"catchall_emails": 10,
"unknown_emails": 10,
"disposable_emails": 0,
"credits_used": 990,
"process_time_seconds": 125.5,
"download_url": "https://api.billionverify.com/v1/verify/file/7143874e-21c5-43c1-96f3-2b52ea41ae6a/results",
"direct_download_url": "https://download.example.com/results_7143874e.csv?signature=...",
"direct_download_expires_at": "2026-02-04T11:35:00Z"
}
}Verifying Webhook Signatures
Webhook signatures use HMAC-SHA256 over this exact string:
{timestamp}.{raw_body}The signature header format is:
X-Webhook-Signature: sha256=<hex_digest>Recommended verification flow:
- Read the raw request body exactly as received.
- Read
X-Webhook-Timestamp. - Build
{timestamp}.{raw_body}. - Compute HMAC-SHA256 with your webhook secret.
- Compare against
X-Webhook-Signature. - Reject stale timestamps to reduce replay risk.
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, timestamp, signature, secret) {
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature =
'sha256=' +
crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
}import hashlib
import hmac
def verify_webhook_signature(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
signed_payload = timestamp.encode() + b"." + raw_body
expected = "sha256=" + hmac.new(
secret.encode(),
signed_payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature, expected)Verification Status
| Status | Meaning | Recommended Action |
|---|---|---|
valid | Email exists and can receive messages | Safe to send |
invalid | Email does not exist or cannot receive | Remove from list |
unknown | Deliverability could not be confirmed | Retry later or use caution |
risky | Deliverable, but the domain has a confirmed abuse history | Remove from list |
disposable | Temporary or disposable mailbox | Usually remove |
catchall | Domain accepts all addresses | Monitor bounce behavior |
role | Role-based mailbox such as info@ or support@ | Usually deliverable, may engage less |
Changed 2026-08-04.
riskypreviously meant "we could not finish verifying" (an internal SMTP capacity fallback) and was retryable and free. That case is now returned asunknownwith the samemx_only_verification_semaphore_timeoutreason, unchanged in behaviour and still free. If your integration treatsriskyas "retry later", update it.
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
0 | 200 | Success |
4000 | 400 | Bad request |
4010 | 401 | Invalid or missing API key |
4020 | 402 | Insufficient credits |
4040 | 404 | Resource not found |
4130 | 413 | File too large |
4290 | 429 | Rate limit exceeded |
1000 | 500 | Internal server error |
Best Practices
1. Keep API Keys Out of Source Code
const apiKey = process.env.BV_API_KEY;2. Use Batch vs File Endpoints Correctly
| Use Case | Endpoint | Limit |
|---|---|---|
| Single real-time verification | /verify/single | 1 email |
| Small synchronous batch | /verify/bulk | 50 emails |
| Large asynchronous processing | /verify/file | 100,000 emails/file |
3. Prefer Webhooks for File Jobs
Polling is supported, but webhooks are a better fit for long-running file verification workflows.