Photo by Chris Ried on Unsplash
Building your own SMTP prober feels like a good idea until you’ve spent two weeks debugging greylisting behavior, 421 vs 450 response codes, and ISPs that accept every RCPT TO without ever bouncing anything.
It’s a solved problem. Use an API.
This is the guide I wish existed when we first integrated external email verification into our stack — what to think about before you write a single line of code.
Sync vs Async: Pick the Right Pattern First
This decision matters more than which provider you choose.
Synchronous (real-time) verification returns a result inline with the user action — typically a form submission or signup. The API call happens before you persist the record. Users wait 200-400ms. You never store a bad address.
Asynchronous (batch) verification takes a list, processes it in the background, and returns results via webhook or polling. Used for cleaning existing lists, verifying imported data, or anywhere a user isn’t waiting on the result.
Most teams need both. The mistake is using async logic where sync is required (you end up storing bad addresses then cleaning them later, which is backwards) or using sync where async belongs (blocking a bulk import on real-time API calls is a great way to time out your users).
Rule of thumb:
- Single-address + user is waiting → sync
- List import or background job → async
Sync Integration: Code Examples
cURL (test your endpoint first)
curl -X POST https://api.bouncekill.com/v1/verify
-H "Authorization: Bearer YOUR_API_KEY"
-H "Content-Type: application/json"
-d '{"email": "user@example.com"}'
Response:
{
"email": "user@example.com",
"status": "VALID",
"score": 96,
"reason": "smtp_verified",
"disposable": false,
"catch_all": false,
"free_provider": false,
"mx_found": true,
"latency_ms": 248
}
Node.js (with fetch)
async function verifyEmail(email: string) {
const response = await fetch('https://api.bouncekill.com/v1/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.BOUNCEKILL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
if (!response.ok) {
throw new Error(`Verification failed: ${response.status}`);
}
return response.json();
}
// Usage in a signup handler
app.post('/signup', async (req, res) => {
const { email } = req.body;
const result = await verifyEmail(email);
if (result.status === 'INVALID' || result.status === 'DISPOSABLE') {
return res.status(422).json({
error: 'Please use a valid business email address.'
});
}
// proceed with account creation
await createUser({ email, ...req.body });
res.json({ success: true });
});
Python
import httpx
import os
async def verify_email(email: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.bouncekill.com/v1/verify",
headers={"Authorization": f"Bearer {os.getenv('BOUNCEKILL_API_KEY')}"},
json={"email": email},
timeout=5.0
)
response.raise_for_status()
return response.json()
# Example: FastAPI signup endpoint
@app.post("/signup")
async def signup(payload: SignupPayload):
result = await verify_email(payload.email)
if result["status"] in ("INVALID", "DISPOSABLE"):
raise HTTPException(status_code=422, detail="Invalid email address")
return await create_user(payload)
Async Integration: Batch + Webhook
For list processing, the pattern is:
1. Submit list → get a job_id
2. Receive webhook when complete (or poll /jobs/{id} if you don’t have a public endpoint)
3. Download results as JSON or CSV
// 1. Submit batch job
const job = await fetch('https://api.bouncekill.com/v1/jobs', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.BOUNCEKILL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
emails: emailList, // array of strings
webhook_url: 'https://your-app.com/webhooks/bouncekill',
}),
}).then(r => r.json());
console.log(job.id); // "job_abc123"
// 2. Verify the webhook signature (HMAC-SHA256)
import crypto from 'crypto';
app.post('/webhooks/bouncekill', (req, res) => {
const signature = req.headers['x-bouncekill-signature'];
const expected = crypto
.createHmac('sha256', process.env.BOUNCEKILL_WEBHOOK_SECRET!)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== expected) {
return res.status(401).send('Invalid signature');
}
const { job_id, results } = req.body;
// process results — update your DB, trigger campaign send, etc.
res.sendStatus(200);
});
Rate Limits and Production Reality
A few things that will bite you if you don’t plan for them:
Per-request timeouts. Real-time SMTP probing can take 1-4 seconds in the worst case (greylisting, slow MX servers). Set your timeout floor at 5 seconds. Don’t use a 2-second timeout and then wonder why 20% of your verifications are timing out.
Concurrency limits. Most providers cap concurrent connections per API key. BounceKill defaults to 10 concurrent real-time requests. If you’re running parallel verification at signup during a spike, implement a simple queue or rate-limiter client-side.
Retry behavior. Got a 429? Exponential backoff with jitter. Don’t just retry after a fixed 1 second — you’ll hammer the rate limit right back.
UNKNOWN results. Some addresses genuinely can’t be verified in real-time — the MX server is greylisting, or it’s a catch-all domain. Have a policy for UNKNOWN results. Common approaches: allow through and mark for async re-verification, or surface a soft-block to the user (“we couldn’t verify this address — please double-check”).
Cost Modeling at Scale
Before you integrate, run the math. At 100,000 daily signups:
| Verification type | Volume | Cost at $0.89/1K |
|---|---|---|
| Real-time signup verification | 100K/day | $89/day |
| Monthly total | 3M/mo | ~$2,670/mo |
That’s not cheap, and it doesn’t have to be. Most teams don’t verify every signup in real-time — they verify on first send (a job invite, a transactional email) rather than at account creation. That typically cuts verified volume by 40-60% while catching the same bad addresses.
Another approach: free-tier signups get soft verification (MX check only, free), paid conversions get full SMTP probing. The bounce risk is on the free tier where it costs you less anyway.
Start Here
BounceKill’s API is free for the first 100 verifications. No credit card. The documentation covers all endpoints, rate limits, and webhook payload schemas.
The integration takes about 20 minutes for a basic sync endpoint. If you get stuck, we have a Discord where the engineering team actually shows up.



Leave a Reply