> ## 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.

# CSV Upload Guide

> Upload and validate CSV files through the dashboard or signed-upload API flow

# CSV Upload Guide

Upload and validate CSV files through the GTMAPIs dashboard or the public signed-upload API flow. Dashboard uploads are designed for interactive CSV jobs; API integrations that need larger files should use the signed-upload flow in [Async Validation Jobs](/api-reference/validate-async).

## Prerequisites

Before uploading a CSV:

* [Create an account](https://www.gtmapis.com/sign-in)
* [Generate an API key](/authentication)
* Ensure you have sufficient credits

## Step-by-Step Process

<Steps>
  <Step title="Prepare Your CSV">
    Your CSV must contain an email column. Other columns are preserved in the output.
  </Step>

  <Step title="Upload to Dashboard">
    Go to [Dashboard](https://www.gtmapis.com/dashboard) and click "Upload CSV"
  </Step>

  <Step title="Map Email Column">
    Select which column contains email addresses
  </Step>

  <Step title="Start Validation">
    Click "Validate" to begin processing
  </Step>

  <Step title="Receive Results">
    Download CSV with validation results or receive via email
  </Step>
</Steps>

## CSV Format Requirements

### Supported Formats

* **File type**: `.csv` only
* **Dashboard max size**: 10,000 emails per upload
* **API large-file flow**: use signed upload preparation and start-processing endpoints
* **Encoding**: UTF-8 recommended
* **Delimiter**: Comma (`,`)

### Example Input CSV

```csv theme={null}
email,first_name,last_name,company
john@company.com,John,Doe,Acme Inc
info@company.com,Info,Team,Acme Inc
invalid@notreal.com,Invalid,User,Fake Corp
sarah@startup.io,Sarah,Williams,Startup LLC
```

### Column Mapping

During upload, you'll select which column contains emails:

* Preview shows first 5 rows
* Click on the column header to select
* System validates format before processing

## Output Format

### Added Validation Columns

Your output CSV includes **11 new columns** with validation results. These are the CSV Result Adapter field names; `email_status`, `credits_consumed`, `is_domain_catch_all`, and `b2b_quality` are compatibility aliases for the canonical validation result contract.

| Column                | Description            | Example Values                                             |
| --------------------- | ---------------------- | ---------------------------------------------------------- |
| `email_status`        | Validation result      | `valid`, `valid_role_based`, `risky`, `invalid`, `unknown` |
| `credits_consumed`    | Credits used           | `0`, `1`                                                   |
| `syntax_valid`        | Syntax validation flag | `true`, `false`                                            |
| `dns_valid`           | DNS/MX validation flag | `true`, `false`                                            |
| `smtp_valid`          | SMTP validation flag   | `true`, `false`                                            |
| `is_domain_catch_all` | Catch-all flag         | `true`, `false`                                            |
| `is_role_based`       | Role-based flag        | `true`, `false`                                            |
| `mx_record`           | Primary MX record      | `aspmx.l.google.com`                                       |
| `mx_provider`         | Email provider         | `Google Workspace`, `Microsoft 365`                        |
| `b2b_quality`         | B2B quality score      | `high`, `low`, `none`, `unknown`                           |
| `domain`              | Email domain           | `company.com`                                              |

### Example Output CSV

```csv theme={null}
email,first_name,last_name,company,email_status,credits_consumed,syntax_valid,dns_valid,smtp_valid,is_domain_catch_all,is_role_based,mx_record,mx_provider,b2b_quality,domain
john@company.com,John,Doe,Acme Inc,valid,1,true,true,true,false,false,aspmx.l.google.com,Google Workspace,high,company.com
info@company.com,Info,Team,Acme Inc,valid_role_based,0,true,true,true,false,true,aspmx.l.google.com,Google Workspace,low,company.com
invalid@notreal.com,Invalid,User,Fake Corp,invalid,0,true,false,false,false,false,,,none,notreal.com
sarah@startup.io,Sarah,Williams,Startup LLC,valid,1,true,true,true,false,false,mx.startup.io,,high,startup.io
```

## Processing Details

### Batch Processing

The system processes emails in batches:

1. **Deduplication**: Removes duplicate emails (case-insensitive)
2. **Batch size**: 100 emails per API request
3. **Delay**: 500ms between batches to avoid rate limits
4. **Retry logic**: Automatic retry on transient failures

### Processing Time

Processing time varies based on DNS caching, SMTP responsiveness, provider budgets, and job size. Use the dashboard or job status endpoint to monitor progress instead of assuming fixed completion times.

### Email Notification

When validation completes:

* Email sent to your account address
* CSV file attached
* Summary statistics included

## Credit Usage

### How Credits Are Reserved and Finalized

CSV jobs reserve credits before validation starts, then finalize the charge after results are known:

1. Upload CSV
2. System reserves credits for unique emails
3. System validates all emails
4. Counts high-quality results
5. Charges only for `result: "valid"` + `b2b_outbound_quality: "high"`
6. Refunds the unused reserved credits
7. Returns results

### Example Credit Usage

**Uploaded CSV**: 1,000 emails

| Result         | Count     | Credits  |
| -------------- | --------- | -------- |
| Valid personal | 400       | 400      |
| Role-based     | 250       | 0 (FREE) |
| Catch-all      | 200       | 0 (FREE) |
| Invalid        | 100       | 0 (FREE) |
| Unknown        | 50        | 0 (FREE) |
| **Total**      | **1,000** | **400**  |

The job may reserve up to the unique email count before processing, but the final charge is 400 credits.

### Insufficient Credits

If you don't have enough credits to reserve the upload:

* Upload is rejected before processing starts
* Error message shows required vs available
* No partial processing (all-or-nothing)

**Solution**: Purchase more credits or reduce batch size

## Best Practices

### Before Uploading

<Accordion title="Clean Your Data">
  Remove obvious invalids and duplicates locally:

  ```javascript theme={null}
  // Remove duplicates
  const unique = [...new Set(emails.map(e => e.toLowerCase()))];

  // Basic syntax filter
  const validFormat = unique.filter(e => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e));

  // Remove common placeholders
  const cleaned = validFormat.filter(e =>
    !e.includes('example.com') &&
    !e.includes('test@') &&
    !e.includes('@test.')
  );
  ```
</Accordion>

<Accordion title="Verify CSV Format">
  Check your CSV before uploading:

  * Proper comma delimiters
  * No missing headers
  * Consistent column count per row
  * UTF-8 encoding (especially for international names)
  * No BOM (Byte Order Mark) issues
</Accordion>

<Accordion title="Test With Small Sample">
  Before processing 10,000 emails:

  1. Extract first 100 rows
  2. Upload test batch
  3. Review results quality
  4. Adjust source or filters if needed
  5. Process full list
</Accordion>

### After Validation

<Accordion title="Filter by Quality">
  Separate emails by B2B quality:

  ```javascript theme={null}
  // High quality for primary campaigns
  const highQuality = results.filter(r =>
    r.result === 'valid' && r.b2b_outbound_quality === 'high'
  );

  // Role-based for secondary outreach
  const roleBased = results.filter(r =>
    r.result === 'valid_role_based'
  );

  // Remove these completely
  const toRemove = results.filter(r =>
    r.result === 'invalid' || r.result === 'risky'
  );
  ```
</Accordion>

<Accordion title="Track Validation History">
  Keep records of validations:

  * Original upload date
  * Source of emails
  * Validation results summary
  * Credits consumed
  * Campaign performance per quality tier

  Re-validate lists every 3-6 months as emails change
</Accordion>

<Accordion title="Measure ROI">
  Compare campaign performance by quality:

  ```
  High Quality Emails:
  - Personal mailbox ownership
  - Credits charged: 1 per result

  Role-Based Emails:
  - Shared inbox behavior
  - Credits charged: 0 per result

  Risky/Invalid/Unknown:
  - Avoid or test carefully depending on status
  - Credits charged: 0 per result
  ```
</Accordion>

## Common Issues

### CSV Upload Fails

**Problem**: "Invalid CSV format" error

**Causes**:

* Non-UTF-8 encoding
* Inconsistent column counts
* Missing headers
* Special characters in data

**Solution**:

1. Open CSV in text editor
2. Check for encoding issues
3. Verify all rows have same column count
4. Remove special characters or escape properly

### No Email Column Detected

**Problem**: System can't find email column

**Causes**:

* Column named something other than "email"
* Emails in wrong format
* Empty column

**Solution**:

1. Rename column to "email" (lowercase)
2. Verify emails are in `user@domain.com` format
3. Check first few rows have valid data

### Processing Takes Too Long

**Problem**: Validation stuck or timing out

**Causes**:

* Large batch size
* Many slow SMTP servers
* Network issues

**Solution**:

1. Check processing status in dashboard
2. Wait for email notification
3. If > 30 minutes, contact support
4. Try smaller batches (\< 5,000 emails)

### Unexpected Credit Charges

**Problem**: Charged more credits than expected

**Causes**:

* Misunderstanding of credit system
* More high-quality emails than estimated
* Duplicates not removed

**Solution**:

1. Check CSV for duplicate emails
2. Review validation results
3. Filter by `credits_charged` column
4. Only `valid` + `b2b_outbound_quality: "high"` are charged

## Programmatic CSV Validation

For automation, use the API directly:

```javascript theme={null}
const fs = require('fs');
const Papa = require('papaparse');

async function validateCSV(filePath) {
  // 1. Read CSV
  const csvData = fs.readFileSync(filePath, 'utf8');
  const parsed = Papa.parse(csvData, { header: true });

  // 2. Extract emails
  const emails = parsed.data.map(row => row.email).filter(Boolean);

  // 3. Validate in batches
  const results = [];
  for (let i = 0; i < emails.length; i += 100) {
    const batch = emails.slice(i, i + 100);

    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);

    // Rate limit delay
    await new Promise(resolve => setTimeout(resolve, 500));
  }

  // 4. Merge results with original data
  const merged = parsed.data.map(row => {
    const result = results.find(r => r.email === row.email);
    return { ...row, ...result };
  });

  // 5. Write output CSV
  const csv = Papa.unparse(merged);
  fs.writeFileSync('output.csv', csv);

  console.log(`Validated ${results.length} emails`);
  return merged;
}
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Upload CSV Now" icon="upload" href="https://www.gtmapis.com/dashboard">
    Start validating your email list
  </Card>

  <Card title="API Integration" icon="code" href="/guides/api-integration">
    Integrate validation into your app
  </Card>
</CardGroup>
