Endpoints // Bulk
Follow a job with long polling until it settles, with its progress, the credits it reserved and spent, and the link to its results, for free.
Credits
Free, also at a zero balance, and never counted against the rate limits.
Introduction
The Get a job endpoint reads one job: it is the links.self path of a job you created. It says how far the job is, what it reserved and spent, and, once it is completed or cancelled, where to download its results. It reads any job of the job history too, such as a List import or a Search export started in the app.
Reading a job is free, also at a zero balance, and never counted against your rate limits, so you can follow a long job at the pace it asks for. A key reads the jobs its permissions allow: Person Enrichment or Contact for a Bulk enrichment job, Lists for a List job, Search for a Search export. A job the key may not read returns 404, as a job that does not exist.
Authorization
| Name | Type | Required | Description |
|---|---|---|---|
apikey | string | Yes | Your API key from the developer dashboard. Pass it via Authorization: Bearer YOUR_API_KEY header. |
Request
The route is GET /v2/jobs/:jobId.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
jobId | string | Yes | The id of the job (UUID) |
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
wait | integer | No | Hold the request up to this many seconds, 0 to 20, while the job is queued or running. It answers as soon as the job settles or pauses, or with the job still running when the wait ends. Defaults to 0. |
How to follow a job
Read the job while its status is queued or running, with wait=20 and an HTTP client timeout above it, for example 30 seconds. While the job moves, the response carries a Retry-After: 2 header: start two reads at least that far apart. A held read already lasts longer, so the next one starts right away.
status | What it means |
|---|---|
queued | Waiting for its turn: read it again |
running | Items are being looked up: read it again |
paused | It waits to be resumed: a member of your workspace paused it in the app, or a List import reached its maxCredits or your balance |
completed | Every item was handled: download the results at links.results |
cancelled | Stopped: the items not started were never charged. The results of the items done are at links.results, where an item still in flight reads running until it ends, within the hour |
failed | The job could not run: error.code says why, and it has no results file |
completed, cancelled and failed are final. The webhook of a job is sent when it reaches one of them.
Response structure
| Field | Type | Description |
|---|---|---|
success | boolean | true if the request was processed successfully |
error | null | Always null on success |
dataobjectThe job
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
id | string | Job id (UUID) |
type | string | enrich_persons (Bulk enrichment, or a file enriched in the app), list_import, list_enrich, list_search_import or search_export, see the job history |
entity | string | persons or companies |
status | string | queued, running, paused, completed, cancelled or failed |
source | string | null | Who started it: api (an API key), mcp (an AI assistant), ask or dashboard (a person in the app). null for a job older than this field. |
name | string | The name given at creation, or the file name of a job started in the app |
listId | string | null | The List a List job works on, null otherwise |
estimatedSecondsRemaining | number | null | Seconds left at the recent pace while the job runs, null when unknown or settled |
error | object | null | { "code": "..." } when the job failed, null otherwise |
createdAt | string | Creation date (ISO 8601) |
startedAt | string | null | When it started running, null while queued |
completedAt | string | null | When it settled, null while it moves |
progressobjectHow far the job is
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
total | number | Items in the job, duplicates included |
processed | number | Items done |
succeeded | number | Items that found at least part of what was asked |
failed | number | Items that found nothing, or could not be looked up |
skipped | number | Items not run: a duplicate, no usable field, an invalid profile URL, not enough credits, or cancelled |
remaining | number | Items left to do |
creditsobjectWhat the job reserved and spent
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
estimated | number | Credits reserved when the job was created, 0 for a job that reserves nothing up front, such as a List import |
used | number | null | Credits spent. A List import counts them as it goes; any other job reads null until it has settled them, then gives back the rest of estimated. |
linksobjectWhere to go next
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
self | string | Path that reads this job |
results | string | null | Path of the results file, CSV or NDJSON, set once an enrich_persons job is completed or cancelled. See Download job results. To read the results while the job runs, see Read job results by page. |
operation | string | null | The List operation a List job runs as, null for other jobs |
quotas and metadata are the blocks of every V2 response: see Usage.
Errors
| Status | Code | Meaning |
|---|---|---|
404 | JOB_NOT_FOUND | No job with this id in your workspace, or one your key may not read |
422 | VALIDATION_ERROR | jobId is not a UUID, or wait is not an integer from 0 to 20 |
Code examples
Code Examples
curl "https://api.reversecontact.com/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60?wait=20" \
-H "Authorization: Bearer YOUR_API_KEY"const jobId = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60";
const headers = { "Authorization": "Bearer YOUR_API_KEY" };
let job;
do {
const response = await fetch(`https://api.reversecontact.com/v2/jobs/${jobId}?wait=20`, { headers });
job = (await response.json()).data;
} while (job.status === "queued" || job.status === "running");
console.log(job.status, `${job.credits.used} of ${job.credits.estimated} credits`, job.links.results);import requests
job_id = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
while True:
job = requests.get(
f"https://api.reversecontact.com/v2/jobs/{job_id}",
headers=headers,
params={"wait": 20},
timeout=30
).json()["data"]
if job["status"] not in ("queued", "running"):
break
print(job["status"], job["credits"]["used"], job["links"]["results"])Example response
Example Response
{
"success": true,
"data": {
"id": "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60",
"type": "enrich_persons",
"entity": "persons",
"status": "completed",
"source": "api",
"name": "CRM export, November",
"listId": null,
"progress": {
"total": 2,
"processed": 2,
"succeeded": 2,
"failed": 0,
"skipped": 0,
"remaining": 0
},
"credits": { "estimated": 9, "used": 6 },
"estimatedSecondsRemaining": null,
"error": null,
"createdAt": "2026-11-04T09:30:00.000Z",
"startedAt": "2026-11-04T09:30:01.000Z",
"completedAt": "2026-11-04T09:31:12.000Z",
"links": {
"self": "/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60",
"results": "/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60/results",
"operation": null
}
},
"error": null,
"quotas": {
"creditsConsumed": 0,
"workspace": {
"id": "ws_abc123",
"credits": { "total": 10000, "used": 4528, "left": 5472 }
}
},
"metadata": {
"requestId": "req_g1e2t3b4a5t6",
"executionTimeMs": 31
}
}{
"success": false,
"data": null,
"error": {
"code": "JOB_NOT_FOUND",
"message": "No job found with that identifier"
},
"quotas": {
"creditsConsumed": 0,
"workspace": {
"id": "ws_abc123",
"credits": { "total": 10000, "used": 4528, "left": 5472 }
}
},
"metadata": {
"requestId": "req_g1e2t3b4a5t7",
"executionTimeMs": 12
}
}