> ## Documentation Index
> Fetch the complete documentation index at: https://gtmapis.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Validate Bulk Emails

> Validate up to 100 emails in a single request

# 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

| 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

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.gtmapis.com/v1/validate/bulk \
    -H "Content-Type: application/json" \
    -H "X-API-Key: gtm_test_1234567890abcdef1234567890abcdef" \
    -d '{
      "emails": [
        "john@company.com",
        "info@company.com",
        "invalid@notreal.com"
      ]
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://api.gtmapis.com/v1/validate/bulk', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': 'gtm_test_1234567890abcdef1234567890abcdef'
    },
    body: JSON.stringify({
      emails: [
        'john@company.com',
        'info@company.com',
        'invalid@notreal.com'
      ]
    })
  });

  const results = await response.json();
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.gtmapis.com/v1/validate/bulk',
      headers={
          'Content-Type': 'application/json',
          'X-API-Key': 'gtm_test_1234567890abcdef1234567890abcdef'
      },
      json={
          'emails': [
              'john@company.com',
              'info@company.com',
              'invalid@notreal.com'
          ]
      }
  )

  results = response.json()
  ```

  ```go Go theme={null}
  package main

  import (
      "bytes"
      "encoding/json"
      "net/http"
  )

  type BulkRequest struct {
      Emails []string `json:"emails"`
  }

  func main() {
      body := BulkRequest{
          Emails: []string{
              "john@company.com",
              "info@company.com",
              "invalid@notreal.com",
          },
      }
      jsonBody, _ := json.Marshal(body)

      req, _ := http.NewRequest("POST", "https://api.gtmapis.com/v1/validate/bulk", bytes.NewBuffer(jsonBody))
      req.Header.Set("Content-Type", "application/json")
      req.Header.Set("X-API-Key", "gtm_test_1234567890abcdef1234567890abcdef")

      client := &http.Client{}
      resp, _ := client.Do(req)
      defer resp.Body.Close()
  }
  ```
</CodeGroup>

## Response

### Success Response (200 OK)

```json theme={null}
{
  "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,
    "high_value": 1,
    "catch_all": 0,
    "credits_reserved": 3,
    "credits_charged": 1,
    "credits_consumed": 1,
    "credits_refunded": 2,
    "credits_per_high_value_validation": 1
  }
}
```

### Response Fields

<ResponseField name="results" type="array" required>
  Array of validation results, one per input email

  Each result has the same fields as the [single validation endpoint](/api-reference/validate-single)
</ResponseField>

<ResponseField name="summary" type="object" required>
  Summary statistics for the bulk validation
</ResponseField>

<ResponseField name="summary.total" type="integer" required>
  Total number of emails validated
</ResponseField>

<ResponseField name="summary.valid" type="integer" required>
  Count of `valid` results (personal emails)
</ResponseField>

<ResponseField name="summary.valid_role_based" type="integer" required>
  Count of `valid_role_based` results
</ResponseField>

<ResponseField name="summary.risky" type="integer" required>
  Count of `risky` results (catch-all domains)
</ResponseField>

<ResponseField name="summary.invalid" type="integer" required>
  Count of `invalid` results
</ResponseField>

<ResponseField name="summary.unknown" type="integer" required>
  Count of `unknown` results
</ResponseField>

<ResponseField name="summary.high_value" type="integer" required>
  Count of High-Value Validations: valid personal emails that are not role-based, catch-all, risky, invalid, or unknown.
</ResponseField>

<ResponseField name="summary.catch_all" type="integer" required>
  Count of results from catch-all domains. These are not High-Value Validations by default.
</ResponseField>

<ResponseField name="summary.credits_reserved" type="integer" required>
  Credits reserved before uncached validation work starts.
</ResponseField>

<ResponseField name="summary.credits_charged" type="integer" required>
  Total credits charged for this bulk validation. This is kept as a compatibility alias for consumed credits.

  Sum of all `credits_charged` values in results
</ResponseField>

<ResponseField name="summary.credits_consumed" type="integer" required>
  Credits consumed after applying GTMAPIs credit policy. For Email Validation this matches `credits_charged`.
</ResponseField>

<ResponseField name="summary.credits_refunded" type="integer" required>
  Reserved credits returned because the corresponding results were role-based, catch-all, invalid, risky, unknown, or otherwise non-chargeable.
</ResponseField>

<ResponseField name="summary.credits_per_high_value_validation" type="number | null" required>
  Effective credits consumed per High-Value Validation. This is `null` when the response contains no high-value validations.
</ResponseField>

## Limits

| Limit                                           | Value                              |
| ----------------------------------------------- | ---------------------------------- |
| Max emails per request                          | 100                                |
| Max emails per async job (`/v1/validate/batch`) | 500,000                            |
| Rate limit                                      | 10,000 requests/minute per API key |

### Exceeding Limits

**Too many emails in one request** (> 100):

```json theme={null}
{
  "error": "Bad Request",
  "message": "Maximum 100 emails per request. Received: 150"
}
```

**Solution**: Split into multiple requests of 100 emails each, or use `/v1/validate/batch` for larger asynchronous jobs.

## Processing Time

Processing time varies with cache state, DNS responsiveness, and SMTP server behavior. Use `/v1/validate/bulk` for synchronous batches up to 100 emails, and use `/v1/validate/batch` or the signed CSV upload flow for larger jobs that need polling and download handoffs.

## Best Practices

### Batch Processing

For large lists (1000+ emails), process in batches:

```javascript theme={null}
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:

```javascript theme={null}
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:

```javascript theme={null}
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}`);
```

## Credit Estimate

Estimate the maximum credits before processing:

```javascript theme={null}
// Estimate maximum credits (assuming all are high-value personal emails)
const emailCount = emails.length;
const maxCredits = emailCount;

console.log(`Max credits: ${maxCredits}`);

// After validation, calculate actual credits charged
const actualCredits = summary.credits_charged;

console.log(`Actual credits: ${actualCredits}`);
console.log(`Credits not charged: ${maxCredits - actualCredits}`);
```

## Common Patterns

### Deduplicate Before Validation

Save credits by removing duplicates:

```javascript theme={null}
// 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:

```javascript theme={null}
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

<CardGroup cols={2}>
  <Card title="Single Validation" icon="envelope" href="/api-reference/validate-single">
    Validate one email at a time
  </Card>

  <Card title="CSV Upload Guide" icon="file-csv" href="/guides/csv-upload">
    Upload and validate CSV files via dashboard
  </Card>
</CardGroup>
