Skip to main content

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

Headers

HeaderRequiredDescription
Content-TypeYesMust be application/json
X-API-KeyYesYour API key (gtm_test_* or gtm_live_*)

Request Body

FieldTypeRequiredDescription
emailsarray[string]YesArray 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

results
array
required
Array of validation results, one per input emailEach result has the same fields as the single validation endpoint
summary
object
required
Summary statistics for the bulk validation
summary.total
integer
required
Total number of emails validated
summary.valid
integer
required
Count of valid results (personal emails)
summary.valid_role_based
integer
required
Count of valid_role_based results
summary.risky
integer
required
Count of risky results (catch-all domains)
summary.invalid
integer
required
Count of invalid results
summary.unknown
integer
required
Count of unknown results
summary.credits_charged
integer
required
Total credits charged for this bulk validationSum of all credits_charged values in results

Limits

LimitValue
Max emails per request100
Max total emails (CSV upload)10,000
Rate limit1000 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

Performance

Processing time scales with batch size:
EmailsAvg TimeNotes
1-10~500msQuick validation
11-50~2sSmall batch
51-100~5sFull 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