POST /v1/validate/bulk
Validate multiple email addresses (up to 100) in a single API request.
Endpoint
POST https://api.gtmapis.com/v1/validate/bulk
| Header | Required | Description |
|---|
Content-Type | Yes | Must be application/json |
X-API-Key | Yes | Your API key (gtm_test_* or gtm_live_*) |
Request Body
| Field | Type | Required | Description |
|---|
emails | array[string] | Yes | Array of emails to validate (max 100) |
Example Request
curl -X POST https://api.gtmapis.com/v1/validate/bulk \
-H "Content-Type: application/json" \
-H "X-API-Key: gtm_test_your_key_here" \
-d '{
"emails": [
"john@company.com",
"info@company.com",
"invalid@notreal.com"
]
}'
Response
Success Response (200 OK)
{
"results": [
{
"email": "john@company.com",
"result": "valid",
"is_role_based": false,
"b2b_outbound_quality": "high",
"credits_charged": 1,
"charge_reason": "Valid personal email with high B2B outbound quality"
},
{
"email": "info@company.com",
"result": "valid_role_based",
"is_role_based": true,
"b2b_outbound_quality": "low",
"credits_charged": 0,
"charge_reason": "Role-based email - free for low B2B value"
},
{
"email": "invalid@notreal.com",
"result": "invalid",
"reason": "Domain has no MX records",
"b2b_outbound_quality": "none",
"credits_charged": 0,
"charge_reason": "Invalid email does not consume credits"
}
],
"summary": {
"total": 3,
"valid": 1,
"valid_role_based": 1,
"invalid": 1,
"risky": 0,
"unknown": 0,
"credits_charged": 1
}
}
Response Fields
Summary statistics for the bulk validation
Total number of emails validated
Count of valid results (personal emails)
Count of valid_role_based results
Count of risky results (catch-all domains)
Total credits charged for this bulk validationSum of all credits_charged values in results
Limits
| Limit | Value |
|---|
| Max emails per request | 100 |
| Max total emails (CSV upload) | 10,000 |
| Rate limit | 1000 requests/minute |
Exceeding Limits
Too many emails in one request (> 100):
{
"error": "Bad Request",
"message": "Maximum 100 emails per request. Received: 150"
}
Solution: Split into multiple requests of 100 emails each
Processing time scales with batch size:
| Emails | Avg Time | Notes |
|---|
| 1-10 | ~500ms | Quick validation |
| 11-50 | ~2s | Small batch |
| 51-100 | ~5s | Full batch |
Note: Times assume a mix of cached/uncached lookups
Best Practices
Batch Processing
For large lists (1000+ emails), process in batches:
async function validateLargeList(emails) {
const batchSize = 100;
const results = [];
for (let i = 0; i < emails.length; i += batchSize) {
const batch = emails.slice(i, i + batchSize);
const response = await fetch('https://api.gtmapis.com/v1/validate/bulk', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.GTMAPIS_API_KEY
},
body: JSON.stringify({ emails: batch })
});
const batchResults = await response.json();
results.push(...batchResults.results);
// Optional: Add delay to avoid rate limits
if (i + batchSize < emails.length) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
return results;
}
Error Handling
Handle errors gracefully:
async function validateBatch(emails) {
try {
const response = await fetch('https://api.gtmapis.com/v1/validate/bulk', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.GTMAPIS_API_KEY
},
body: JSON.stringify({ emails })
});
if (!response.ok) {
if (response.status === 429) {
// Rate limit - wait and retry
await new Promise(resolve => setTimeout(resolve, 60000));
return validateBatch(emails);
}
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return await response.json();
} catch (error) {
console.error('Validation failed:', error);
throw error;
}
}
Filtering Results
Filter by quality for campaign use:
const { results, summary } = await validateBulk(emails);
// Only high-quality emails for primary campaign
const highQuality = results.filter(r =>
r.result === 'valid' && r.b2b_outbound_quality === 'high'
);
// Role-based emails for secondary campaign
const roleBased = results.filter(r =>
r.result === 'valid_role_based'
);
// Remove invalids and risky
const toRemove = results.filter(r =>
r.result === 'invalid' || r.result === 'risky'
);
console.log(`High quality: ${highQuality.length}`);
console.log(`Role-based: ${roleBased.length}`);
console.log(`To remove: ${toRemove.length}`);
console.log(`Credits charged: ${summary.credits_charged}`);
Cost Calculation
Calculate costs before processing:
// Estimate maximum cost (assuming all are valid personal emails)
const maxEmails = 10000;
const maxCostPerEmail = 0.001; // $0.001 per credit
const maxCost = maxEmails * maxCostPerEmail; // $10
console.log(`Max cost: $${maxCost.toFixed(2)}`);
// After validation, calculate actual cost
const actualCredits = summary.credits_charged;
const actualCost = actualCredits * maxCostPerEmail;
console.log(`Actual cost: $${actualCost.toFixed(2)}`);
console.log(`Savings: $${(maxCost - actualCost).toFixed(2)} (${((1 - actualCost / maxCost) * 100).toFixed(0)}%)`);
Common Patterns
Deduplicate Before Validation
Save credits by removing duplicates:
// Remove duplicates (case-insensitive)
const uniqueEmails = [...new Set(
emails.map(e => e.toLowerCase())
)];
console.log(`Original: ${emails.length}`);
console.log(`Unique: ${uniqueEmails.length}`);
console.log(`Duplicates removed: ${emails.length - uniqueEmails.length}`);
// Validate unique emails only
const results = await validateBulk(uniqueEmails);
Progress Tracking
Track progress for large batches:
async function validateWithProgress(emails, onProgress) {
const batchSize = 100;
const results = [];
for (let i = 0; i < emails.length; i += batchSize) {
const batch = emails.slice(i, i + batchSize);
const batchResults = await validateBatch(batch);
results.push(...batchResults.results);
// Report progress
const progress = Math.min(100, ((i + batchSize) / emails.length) * 100);
onProgress(progress, results.length, emails.length);
}
return results;
}
// Usage
await validateWithProgress(emails, (progress, validated, total) => {
console.log(`Progress: ${progress.toFixed(0)}% (${validated}/${total})`);
});
Next Steps