Upload an email list
Uploads a CSV file and starts a bulk verification job in the background. Save the job id from the response; every later call needs it.
/api/v1/api-validation/bulkRequest
Send multipart/form-data with one file field.
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | A CSV file up to 50 MB, with content type text/csv. |
The job is named after the file, without the extension: q3-webinar-leads.csv becomes q3-webinar-leads in the dashboard.
CSV format
name,email,company
Jane Doe,[email protected],Acme Corp
Sam Lee,[email protected],Tech IncA file without a header row is read as if its first address were the header, and fails with Email column not found in the file.
Examples
curl "https://simplecold.com/api/v1/api-validation/bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "[email protected];type=text/csv"import { readFile } from "node:fs/promises";
import { basename } from "node:path";
const API = "https://simplecold.com/api/v1";
const apiKey = process.env.SIMPLECOLD_API_KEY;
async function upload(path) {
const form = new FormData();
const blob = new Blob([await readFile(path)], { type: "text/csv" });
form.append("file", blob, basename(path));
const res = await fetch(`${API}/api-validation/bulk`, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` }, // no Content-Type: fetch adds the multipart boundary
body: form,
});
const body = await res.json();
if (!res.ok) throw new Error(`upload failed: ${body.message ?? body.error}`);
return body.payload;
}
const job = await upload("q3-webinar-leads.csv");
console.log(`job ${job.id}: ${job.totalEmails} emails, ${job.status}`);import os
import requests
API = "https://simplecold.com/api/v1"
API_KEY = os.environ["SIMPLECOLD_API_KEY"]
with open("q3-webinar-leads.csv", "rb") as f:
res = requests.post(
f"{API}/api-validation/bulk",
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": ("q3-webinar-leads.csv", f, "text/csv")},
timeout=300,
)
body = res.json()
if not res.ok:
raise RuntimeError(f"upload failed: {body.get('message') or body.get('error')}")
job = body["payload"]
print(job["id"], job["totalEmails"], job["status"])<?php
const API = 'https://simplecold.com/api/v1';
$ch = curl_init(API . '/api-validation/bulk');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
// No Content-Type header: curl sets multipart/form-data with the boundary.
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('SIMPLECOLD_API_KEY')],
CURLOPT_POSTFIELDS => [
'file' => new CURLFile('q3-webinar-leads.csv', 'text/csv', 'q3-webinar-leads.csv'),
],
CURLOPT_TIMEOUT => 300,
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException('upload failed: ' . ($body['message'] ?? $body['error'] ?? $status));
}
$job = $body['payload'];
echo $job['id'], ' ', $job['totalEmails'], ' ', $job['status'], PHP_EOL;;type=text/csv sets the content type of the file part. The endpoint only accepts text/csv, and some curl builds guess a different type from the extension, so set it explicitly.
Don't set a Content-Type header yourself when sending FormData. A hand-written header without the boundary breaks the upload.
{
"code": "SUCCESS",
"message": "Successfully performed operation.",
"payload": {
"id": "b0f6c2a4-6a3e-4c55-9d1e-2f8a7c1b9e40",
"name": "q3-webinar-leads",
"totalEmails": 4812,
"totalValidEmails": 0,
"totalCreditsUsed": 4812,
"status": "IN PROGRESS",
"createdAt": "2026-07-30T09:14:02.511Z",
"updatedAt": "2026-07-30T09:14:02.511Z"
},
"requestId": "1d9f..."
}The payload is the new job record, so it can also contain bookkeeping fields such as orgId, userId and apiKeyId.
Response fields
| Field | Type | Description |
|---|---|---|
id | string | The job ID. Use it for status and download. |
name | string | The file name without its extension. |
totalEmails | number | Unique addresses in the file, after empty rows and duplicates are removed. |
totalValidEmails | number | Addresses with the reason code mailbox_found. 0 at upload; filled in when the job completes. |
totalCreditsUsed | number | Credits reserved for the job (one per unique address). |
status | string | IN PROGRESS for API uploads. See job statuses. |
createdAt, updatedAt | string | ISO 8601 timestamps. |
Errors
| HTTP | Message | Cause |
|---|---|---|
400 | File is required | No file field in the multipart body. |
400 | Invalid file type. Only text/csv are allowed. | The file part's content type isn't text/csv. |
400 | File too large | The file is over 50 MB. Split it. |
400 | Email column not found in the file. | No header matches an accepted email column name. |
400 | No email addresses found in the file. | The email column is empty. |
400 | Insufficient credits. / Please buy credits. | Your balance can't cover the file. No job is created. |
401 | Invalid API key and others | See authentication errors. |
429 | Rate limit exceeded. Maximum 10 uploads per minute per API key. | Over 10 uploads per minute with this API key. Wait Retry-After seconds. |
Error bodies use { "code": "FAILURE", "message": "...", "data": null }. The 429 body has code: "RATE_LIMITED", details in data, plus status and error (see Rate limits).
Next
Poll the job status until it is COMPLETED, then download the results.