Skip to content

API reference/bulk

Resume a bulk job that ran out of credits

POST/api/v1/email/bulk/{jobId}/resume

authentication
Secret API key as a bearer token
billing
Free to call. The resumed run bills the rows it reaches exactly as the first one did, and a row answered in the last seven days is a free repeat.

Runs a job that stopped when the credit balance ran out again, from the top of the list it was given, instead of asking you to upload it a second time. Rows verified in the last seven days come back as free repeats, so only the rows the stopped run never reached are charged.

The job keeps its id, its webhook and its cumulative credits_used, which is the real spend on the account. processed and the verdict counts restart at zero: the part files they were counted from are deleted when a run settles, so the resumed run rebuilds them, and the result file the stopped run left is replaced as the rows come back.

Only a run whose stopped_reason is insufficient_credits and whose status has settled can be resumed; anything else answers 409 JOB_NOT_RESUMABLE, and so does a second resume of a run that is already going again — claiming the run is one conditional write, so two calls can never queue it twice. A run whose list has passed out of the retention window answers 409 JOB_INPUT_EXPIRED. An empty balance answers 402 INSUFFICIENT_CREDITS rather than queueing a run that would stop again on the first row it never reached.

Parameters

name in type description
jobIdrequiredpathinteger

Example request

curl -X POST https://spaw.co/api/v1/email/bulk/{jobId}/resume \
  -H "Authorization: Bearer sk_live_…"
const response = await fetch('https://spaw.co/api/v1/email/bulk/{jobId}/resume', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_live_…',
  },
});
const result = await response.json();
import requests

response = requests.post(
    'https://spaw.co/api/v1/email/bulk/{jobId}/resume',
    headers={'Authorization': 'Bearer sk_live_…'},
)
result = response.json()
$ch = curl_init('https://spaw.co/api/v1/email/bulk/{jobId}/resume');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_live_…'],
]);
$result = json_decode(curl_exec($ch), true);
from spaw import Client

client = Client('sk_live_…')
result = client.resume_bulk_job(job_id)
import Spaw from 'spaw';

const spaw = new Spaw({ apiKey: 'sk_live_…' });
const result = await spaw.resumeBulkJob(jobId);
use Spaw\Client;

$spaw = new Client('sk_live_…');
$result = $spaw->resumeBulkJob($jobId);

This endpoint has no console on its page. It writes to your account. A documentation page can show you the request; making the change is for the dashboard or for a call you make yourself.

Responses

202The list is queued again.

{
    "success": true,
    "data": {
        "job": {
            "id": 512,
            "status": "queued",
            "total": 2000,
            "duplicate_count": 12,
            "processed": 0,
            "deliverable": 0,
            "risky": 0,
            "undeliverable": 0,
            "credits_used": 750,
            "stopped_reason": null,
            "cancel_requested": false,
            "webhook_status": null,
            "created_at": "2026-09-03T10:12:44+00:00",
            "finished_at": null
        }
    }
}

401The key is missing, malformed, or revoked.

{
    "success": false,
    "error": {
        "code": "UNAUTHENTICATED",
        "message": "Provide a valid API key as a bearer token.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

402The balance is empty. The lookup did not run.

{
    "success": false,
    "error": {
        "code": "INSUFFICIENT_CREDITS",
        "message": "Your credit balance is empty.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

404No such record on this account.

{
    "success": false,
    "error": {
        "code": "NOT_FOUND",
        "message": "No record with that id on this account.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

409The run is not in a state a resume can recover (`JOB_NOT_RESUMABLE`), or its list is no longer stored (`JOB_INPUT_EXPIRED`).

{
    "success": false,
    "error": {
        "code": "JOB_NOT_RESUMABLE",
        "message": "Only a run that stopped because the credit balance ran out can be resumed, and only once it has finished settling.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

429Over 5 requests per second for the key. Retry after the limit resets.

{
    "success": false,
    "error": {
        "code": "RATE_LIMITED",
        "message": "Too many requests. Retry after the limit resets.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

Error codes

Failures answer { success: false, error: { code, message, request_id } }. Each code has its own page.

markdown version·openapi.json