> For the complete documentation index, see [llms.txt](https://docs.noparam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.noparam.com/bulk-jobs.md).

# Bulk Jobs

Use this for large lists. Jobs run on the server, so the client does not have to keep a request open. `POST /api/v1/verify` and `POST /api/v1/verify/bulk` are unchanged.

All endpoints take `Authorization: Bearer <API_KEY>` and share the usual rate limit (2 requests/second).

### Create a job

`POST /api/v1/verify/bulk/jobs`

```json
{ "emails": ["a@example.com", "b@example.com"], "webhook_url": "https://example.com/hooks/noparam" }
```

* `webhook_url` is optional. It must be https and point to a public address.
* Unique emails are charged, compared case-insensitively. Duplicate rows still get a result.
* The whole job is reserved against your monthly quota up front. If it is canceled or fails, rows that were not processed are refunded.
* If the list has more unique emails than your plan's per-file limit (Hobby 100, Starter 5,000, Pro 25,000), you get `422`. If it needs more than your remaining quota, you get `429`. Nothing is truncated.

Response `202`:

```json
{
  "job_id": "cmv...",
  "status": "queued",
  "total": 2,
  "processed": 0,
  "progress": 0,
  "charged": 2,
  "stats": {},
  "summary": { "deliverable": 0, "undeliverable": 0, "risky": 0, "invalid": 0 },
  "webhook_url": "https://example.com/hooks/noparam",
  "created_at": "..."
}
```

`status` is one of `queued`, `processing`, `completed`, `failed` or `canceled`.

Listing returns every job on the account, including ones started from the dashboard (`"source": "dashboard"`) or from the Mailchimp integration (`"source": "mailchimp"`). Mailchimp jobs also have a `mailchimp` object: `{ "list_id", "list_name", "tagged", "tag_failed" }`. Those jobs stay `processing` until every verified contact has been tagged on Mailchimp with its status.

### Status and results

`GET /api/v1/verify/bulk/jobs/{job_id}?page=1&limit=100`

Returns the job fields above plus a page of results. `results` holds the processed rows in input order, and each one has an `index`. `limit` can go up to 1000. `has_more` is true when there are more pages, or when more results are still being processed.

`GET /api/v1/verify/bulk/jobs/{job_id}?format=csv` (or `format=xlsx`) downloads every row.

### Data retention

Emails, file columns and results are kept for 30 days after a job ends, so you can download them, and are then deleted automatically. Deleting a job removes them right away. Each job's `results_expire_at` gives the deletion date. Once `results_deleted` is `true`, the job still shows its counts (`stats`, `summary`), `results` is empty, and downloads return `410 Gone`.

The real-time endpoints (`POST /api/v1/verify` and `POST /api/v1/verify/bulk`) never store emails.

### List and cancel

* `GET /api/v1/verify/bulk/jobs?limit=20&offset=0`: your jobs, newest first.
* `DELETE /api/v1/verify/bulk/jobs/{job_id}`: cancels a queued or running job. Returns `409` if the job has already finished.

### Webhook

When a job completes or fails, NoParam sends a `POST` to `webhook_url`:

```json
{ "event": "bulk_job.completed", "job": { "job_id": "...", "status": "completed" } }
```

The `job` object has the same fields as the status response. `event` is `bulk_job.completed` or `bulk_job.failed`.

The header `X-NoParam-Signature: t=<unix timestamp>,v1=<hex>` is an HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the webhook signing secret shown on the dashboard API key page. Any 2xx response counts as delivered. Otherwise the webhook is retried twice more, after 5s and after 30s.
