Endpoints // Bulk
Read every job of your workspace, newest first, whoever started it, from the API, an AI assistant or the app, with filters by type, status and List, for free.
Credits
Free.
Introduction
The Job history endpoint returns the background jobs of your workspace, newest first: the jobs of Bulk enrichment, and the jobs that run the same way in the app or through a List, such as a List import or a Search exported as a file. It covers every job, whoever started it: an API key, an AI assistant through the MCP server, Ask or a member of your team in the app. Each item has the fields of Get a job.
This endpoint is free (0 credits), also at a zero balance. It counts toward your per-minute rate limit like any other call.
A key sees the jobs its permissions allow: Person Enrichment or Contact for the enrich_persons jobs, Lists for the List jobs, Search for the Search exports. A key with no matching permission gets an empty page.
Authorization
| Name | Type | Required | Description |
|---|---|---|---|
apikey | string | Yes | Your API key from the developer dashboard. Pass it via Authorization: Bearer YOUR_API_KEY header. |
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
type | string | No | Only these types, comma-separated, see Job types |
status | string | No | Only these statuses, comma-separated: queued, running, paused, completed, cancelled, failed |
listId | string | No | Only the jobs of this List (UUID) |
perPage | integer | No | Jobs per page, from 1 to 100. Defaults to 50. |
cursor | string | No | The nextCursor of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns 400. |
The route is GET /v2/jobs. A type or status that is not in its list returns 400 JOB_INVALID_INPUT with the accepted values.
Job types
type | What the job does |
|---|---|
enrich_persons | Looks up people and returns a CSV: Bulk enrichment, or a file enriched with Bulk Import in the app |
list_import | Imports Signals or a file into a List: Import members and data |
list_enrich | Buys data for the members of a List |
list_search_import | Adds the results of a Search to a List |
search_export | Exports the results of a Search as a file |
Only an enrich_persons job has a results CSV at Download job results. The members of a List job are read with List members, and its links.operation is the List operation it runs as.
Response structure
| Field | Type | Description |
|---|---|---|
success | boolean | true if the request was processed successfully |
error | null | Always null on success |
dataobjectOne page of jobs
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
items | Job | The jobs of this page, newest first, with the fields of Get a job |
nextCursor | string | null | Send it back as cursor for the next page, null on the last page |
quotas and metadata are the blocks of every V2 response: see Usage.
Errors
| Status | Code | Meaning |
|---|---|---|
400 | JOB_INVALID_INPUT | An unknown type or status, a perPage out of range, or a cursor that is malformed or sent with other filters |
422 | VALIDATION_ERROR | listId is not a UUID |
Code examples
Code Examples
curl "https://api.reversecontact.com/v2/jobs?type=enrich_persons&status=completed,cancelled" \
-H "Authorization: Bearer YOUR_API_KEY"const headers = { "Authorization": "Bearer YOUR_API_KEY" };
const jobs = [];
let cursor = null;
// Read every completed or cancelled job, page by page.
do {
const query = new URLSearchParams({ type: "enrich_persons", status: "completed,cancelled", perPage: "100" });
if (cursor) query.set("cursor", cursor);
const response = await fetch(`https://api.reversecontact.com/v2/jobs?${query}`, { headers });
const { data } = await response.json();
jobs.push(...data.items);
cursor = data.nextCursor;
} while (cursor);
for (const job of jobs) console.log(job.name, job.status, job.credits.used);import requests
headers = {"Authorization": "Bearer YOUR_API_KEY"}
params = {"type": "enrich_persons", "status": "completed,cancelled", "perPage": 100}
jobs = []
while True:
data = requests.get("https://api.reversecontact.com/v2/jobs", headers=headers, params=params).json()["data"]
jobs.extend(data["items"])
if not data["nextCursor"]:
break
params["cursor"] = data["nextCursor"]
for job in jobs:
print(job["name"], job["status"], job["credits"]["used"])Example response
Example Response
{
"success": true,
"data": {
"items": [
{
"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
}
},
{
"id": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"type": "list_import",
"entity": "persons",
"status": "running",
"source": "dashboard",
"name": "Trade show leads.csv",
"listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40",
"progress": { "total": 500, "processed": 120, "succeeded": 114, "failed": 6, "skipped": 0, "remaining": 380 },
"credits": { "estimated": 0, "used": 456 },
"estimatedSecondsRemaining": 240,
"error": null,
"createdAt": "2026-11-04T09:12:00.000Z",
"startedAt": "2026-11-04T09:12:02.000Z",
"completedAt": null,
"links": {
"self": "/v2/jobs/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"results": null,
"operation": "/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress"
}
}
],
"nextCursor": null
},
"error": null,
"quotas": {
"creditsConsumed": 0,
"workspace": {
"id": "ws_abc123",
"credits": { "total": 10000, "used": 4528, "left": 5472 }
}
},
"metadata": {
"requestId": "req_h1i2s3t4o5r6",
"executionTimeMs": 44
}
}