Endpoints // Bulk
Read the results of a Bulk enrichment job a page at a time, in the order sent, from the start of the run, with a cursor that never skips an item, for free.
Credits
Free, also at a zero balance.
Each page counts toward the rate limits.
Introduction
The Read job results by page endpoint returns the results of a job you created as JSON, a page at a time, in the order you sent the items. Each item has the same fields as a line of the NDJSON results file: your own fields and externalId, what happened, what it cost, and the person found.
You can read it from the start of the run: there is no need to wait for the job to settle. While the job runs, a page stops at the first item not finished yet, so a cursor never skips an item and every item you read is final. When the next item is still running, the page is empty, nextCursor stays set and the response carries a Retry-After header: read the same cursor again after that many seconds. nextCursor is null once nothing more will come. A cursor keeps working after the job settles, so you can stop reading and pick up where you left off.
A cancelled job still finishes the items it had started, within the hour: a page waits for them the same way. While the job is paused, the page stays empty until it runs again.
This endpoint is free (0 credits), also at a zero balance. Each page counts toward your rate limits like any other call, and while the job runs, its items take up to 80% of your per-minute limit: read with limit=100, at the Retry-After pace. The TypeScript and Python SDKs and rc bulk rows --all wait out a 429 on their own. The key needs Person Enrichment or Contact, the permissions that read an enrich_persons job.
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/rows.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
jobId | string | Yes | The id of the job (UUID) |
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
cursor | string | No | nextCursor of the previous page, up to 512 characters. Without it, the page starts at the first item. |
limit | integer | No | Items per page, 1 to 100. Defaults to 100. |
A page can hold fewer items than limit: it stops at the first item not finished, and before 2 MB of JSON.
Response structure
| Field | Type | Description |
|---|---|---|
success | boolean | true if the request was processed successfully |
error | null | Always null on success |
dataobjectOne page of results
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
items | array | The items of this page, in the order sent, see below |
nextCursor | string | null | Pass it as cursor to read on. null once nothing more will come. |
status | string | The job status when the page was read, as in Get a job |
itemsobjectOne item of the job
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
row | number | Position of the item in the job, from 1 |
input | object | The item as you sent it, externalId included, empty fields left out |
status | string | What happened to the item, as the rc_status column: see Download job results |
message | string | The same in a sentence, with the credits the item cost |
creditsConsumed | number | Credits this item cost |
person | object | null | The person found, in the shape of Enrich profile. null when no one was found or the job did not ask for the profile. |
isFullProfile | boolean | true when person is the full profile |
linkedinUrl | string | Without profile in the job: the Social profile URL the finders searched from |
contactEmail | object | With contact_email: status (found, not_found, error or skipped, null when the finder did not run), value, and personalValue with emailType: "any" |
contactPhone | object | With contact_phone: status (found, not_found, error or skipped, null when the finder did not run), value and type |
quotas and metadata are the blocks of every V2 response: see Usage.
Errors
| Status | Code | Meaning |
|---|---|---|
400 | JOB_INVALID_INPUT | cursor is not a position of this job, such as a cursor of another job |
404 | JOB_NOT_FOUND | No enrich_persons job with this id in your workspace, or one your key may not read. A List job or a Search export has no results here. |
409 | JOB_RESULTS_FORMAT_UNAVAILABLE | The results of this job were stored before this endpoint existed: download them as CSV with Download job results |
422 | VALIDATION_ERROR | jobId is not a UUID, limit is not an integer from 1 to 100, or cursor is longer than 512 characters |
429 | JOB_ROWS_BUSY | Too many pages are being read at once: wait error.details.retryAfter seconds, given in the Retry-After header too, then read again |
A 429 can also come from your rate limits: see Rate limits.
Code examples
Code Examples
curl "https://api.reversecontact.com/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60/rows?limit=100" \
-H "Authorization: Bearer YOUR_API_KEY"const jobId = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60";
const headers = { "Authorization": "Bearer YOUR_API_KEY" };
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
let cursor = null;
do {
const query = new URLSearchParams({ limit: "100", ...(cursor ? { cursor } : {}) });
const response = await fetch(`https://api.reversecontact.com/v2/jobs/${jobId}/rows?${query}`, { headers });
if (response.status === 429) {
await sleep(Number(response.headers.get("retry-after") ?? 2));
continue;
}
const { data } = await response.json();
for (const item of data.items) console.log(item.row, item.input.externalId, item.status);
if (data.items.length === 0 && data.nextCursor) {
if (data.status === "paused") break;
await sleep(Number(response.headers.get("retry-after") ?? 2));
}
cursor = data.nextCursor;
} while (cursor);import time
import requests
job_id = "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
cursor = None
while True:
params = {"limit": 100, **({"cursor": cursor} if cursor else {})}
response = requests.get(
f"https://api.reversecontact.com/v2/jobs/{job_id}/rows", headers=headers, params=params
)
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", 2)))
continue
data = response.json()["data"]
for item in data["items"]:
print(item["row"], item["input"].get("externalId"), item["status"])
if not data["items"] and data["nextCursor"]:
if data["status"] == "paused":
break
time.sleep(int(response.headers.get("Retry-After", 2)))
cursor = data["nextCursor"]
if cursor is None:
breakExample response
The example cuts person after its first fields: a page carries the whole profile.
Example Response
{
"success": true,
"data": {
"items": [
{
"row": 1,
"input": { "linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001" },
"status": "enriched",
"message": "Profile and email found (4 credits)",
"creditsConsumed": 4,
"person": {
"id": "prs_9f8e7d6c5b4a",
"firstName": "Jane",
"lastName": "Doe",
"linkedinUrl": "https://www.social.com/in/janedoe",
"headline": "VP of Marketing @ Acme Corp"
},
"isFullProfile": true,
"contactEmail": { "status": "found", "value": "jane.doe@acme.com", "personalValue": null }
},
{
"row": 2,
"input": { "firstName": "John", "lastName": "Smith", "companyDomain": "acme.com", "externalId": "crm-002" },
"status": "partial",
"message": "Profile found, no email (2 credits)",
"creditsConsumed": 2,
"person": {
"id": "prs_1a2b3c4d5e6f",
"firstName": "John",
"lastName": "Smith",
"linkedinUrl": "https://www.social.com/in/john-smith",
"headline": "Head of Sales at Acme"
},
"isFullProfile": true,
"contactEmail": { "status": "not_found", "value": null, "personalValue": null }
}
],
"nextCursor": "eyJ2IjoxLCJyb3ciOjN9",
"status": "running"
},
"error": null,
"quotas": {
"creditsConsumed": 0,
"workspace": {
"id": "ws_abc123",
"credits": { "total": 10000, "used": 4531, "left": 5469 }
}
},
"metadata": {
"requestId": "41f7ce39-14eb-4f81-a499-bcbd97d437ed",
"executionTimeMs": 42
}
}{
"success": true,
"data": {
"items": [],
"nextCursor": "eyJ2IjoxLCJyb3ciOjN9",
"status": "running"
},
"error": null,
"quotas": {
"creditsConsumed": 0,
"workspace": {
"id": "ws_abc123",
"credits": { "total": 10000, "used": 4531, "left": 5469 }
}
},
"metadata": {
"requestId": "6f4a5cba-ef9e-470b-ab60-821d4781673c",
"executionTimeMs": 18
}
}{
"success": false,
"data": null,
"error": {
"code": "JOB_ROWS_BUSY",
"message": "Too many result pages read at once: retry in 2 seconds",
"details": { "retryAfter": 2 }
},
"quotas": {
"creditsConsumed": 0,
"workspace": {
"id": "ws_abc123",
"credits": { "total": 10000, "used": 4531, "left": 5469 }
}
},
"metadata": {
"requestId": "def7c408-4e1c-4b83-8858-8e87de88925a",
"executionTimeMs": 3
}
}