Get the job status
Returns a bulk job's status, progress and an estimate of when it will finish. Poll it until the job is COMPLETED.
/api/v1/api-validation/bulk/:id/statusPath parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The job id returned by the upload. |
Request
curl "https://simplecold.com/api/v1/api-validation/bulk/b0f6c2a4-6a3e-4c55-9d1e-2f8a7c1b9e40/status" \
-H "Authorization: Bearer YOUR_API_KEY"{
"code": "SUCCESS",
"message": "Successfully performed operation.",
"payload": {
"status": "IN PROGRESS",
"totalProcessed": 2100,
"progressPercent": 43.6,
"etaSeconds": 310,
"lastUpdatedAt": "2026-07-30T09:19:40.002Z",
"nextUpdateAt": "2026-07-30T09:20:40.002Z",
"queuedChunks": 0
},
"requestId": "7e21..."
}When every address has a result, status becomes COMPLETED and progressPercent reaches 100.
The status response does not repeat the job's name or totals. Keep those from the upload response.
Response fields
| Field | Type | Description |
|---|---|---|
status | string | IN PROGRESS, COMPLETED, CANCELING, CANCELED, FAILED or QUEUE. Note the space in IN PROGRESS. See job statuses. |
totalProcessed | number | Addresses with a result so far. |
progressPercent | number | totalProcessed as a percentage of the job's addresses, to one decimal place. |
etaSeconds | number | null | Estimated seconds left while the job is running, or null when there isn't enough data for an estimate yet (and once the job has finished). |
lastUpdatedAt | string | null | When the progress numbers last changed. |
nextUpdateAt | string | null | When the progress numbers will next change. Polling before this time returns the same numbers. |
queuedChunks | number | Parts of the job still waiting to be sent for checking. |
Polling
Progress only moves at nextUpdateAt, and status has its own limit of 120 requests per minute per API key. So wait until nextUpdateAt (plus a second), with a floor of about 5 seconds and a ceiling of about 2 minutes. If nextUpdateAt is null, use etaSeconds capped at a minute, or 15 seconds.
Stop when status is COMPLETED or CANCELED (you can download from both) or FAILED (retry the job from the dashboard).
const API = "https://simplecold.com/api/v1";
const auth = { Authorization: `Bearer ${process.env.SIMPLECOLD_API_KEY}` };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function nextDelayMs(p) {
let wait = 15_000;
if (p.nextUpdateAt) wait = new Date(p.nextUpdateAt).getTime() - Date.now() + 1_000;
else if (p.etaSeconds != null) wait = Math.min(p.etaSeconds * 1000, 60_000);
return Math.min(120_000, Math.max(5_000, wait));
}
export async function waitForJob(id) {
for (;;) {
const res = await fetch(`${API}/api-validation/bulk/${id}/status`, { headers: auth });
if (res.status === 429) {
await sleep(Number(res.headers.get("retry-after") ?? 60) * 1000);
continue;
}
const body = await res.json();
if (!res.ok) throw new Error(`status failed: ${body.message}`);
const p = body.payload;
console.log(`${p.status} ${p.progressPercent}% eta=${p.etaSeconds ?? "?"}s`);
if (p.status === "COMPLETED" || p.status === "CANCELED") return p;
if (p.status === "FAILED") throw new Error("job failed; retry it from the dashboard");
await sleep(nextDelayMs(p));
}
}import os
import time
from datetime import datetime, timezone
import requests
API = "https://simplecold.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SIMPLECOLD_API_KEY']}"
def next_delay(p):
wait = 15.0
if p.get("nextUpdateAt"):
when = datetime.fromisoformat(p["nextUpdateAt"].replace("Z", "+00:00"))
wait = (when - datetime.now(timezone.utc)).total_seconds() + 1
elif p.get("etaSeconds") is not None:
wait = min(p["etaSeconds"], 60)
return max(5, min(120, wait))
def wait_for_job(job_id):
while True:
res = session.get(f"{API}/api-validation/bulk/{job_id}/status", timeout=30)
if res.status_code == 429:
time.sleep(int(res.headers.get("Retry-After", 60)))
continue
res.raise_for_status()
p = res.json()["payload"]
print(p["status"], f"{p['progressPercent']}%", "eta", p["etaSeconds"])
if p["status"] in ("COMPLETED", "CANCELED"):
return p
if p["status"] == "FAILED":
raise RuntimeError("job failed; retry it from the dashboard")
time.sleep(next_delay(p))<?php
const API = 'https://simplecold.com/api/v1';
function next_delay(array $p): float
{
$wait = 15.0;
if (!empty($p['nextUpdateAt'])) {
$wait = strtotime($p['nextUpdateAt']) - time() + 1;
} elseif ($p['etaSeconds'] !== null) {
$wait = min($p['etaSeconds'], 60);
}
return max(5, min(120, $wait));
}
function wait_for_job(string $jobId): array
{
while (true) {
$retryAfter = 60;
$ch = curl_init(API . "/api-validation/bulk/$jobId/status");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('SIMPLECOLD_API_KEY')],
CURLOPT_TIMEOUT => 30,
CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$retryAfter) {
if (stripos($line, 'retry-after:') === 0) {
$retryAfter = (int) trim(substr($line, 12));
}
return strlen($line);
},
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status === 429) {
sleep($retryAfter);
continue;
}
if ($status >= 400) {
throw new RuntimeException('status failed: ' . ($body['message'] ?? $status));
}
$p = $body['payload'];
echo "{$p['status']} {$p['progressPercent']}% eta=" . ($p['etaSeconds'] ?? '?') . "s\n";
if (in_array($p['status'], ['COMPLETED', 'CANCELED'], true)) {
return $p;
}
if ($p['status'] === 'FAILED') {
throw new RuntimeException('job failed; retry it from the dashboard');
}
usleep((int) (next_delay($p) * 1_000_000));
}
}Errors
| HTTP | Message | Cause |
|---|---|---|
400 | Job not found. | No job with this id in your organization. |
401 | Invalid API key and others | See authentication errors. |
429 | Rate limit exceeded. Maximum 120 status requests per minute per API key. | Over 120 status requests per minute with this API key. Wait Retry-After seconds. |
Next
When the job is COMPLETED, download the results.