Endpoints // Lists
Follow an import with long polling, read why it paused and what to do, and page through its per-line results, for free.
Credits
Free, also at a zero balance, and never counted against the rate limits.
Introduction
The Read a List operation endpoint reads an operation started by an import: it is the pollUrl of the import. It says how far the operation is, what it has charged, and what to do next with clientAction. With rows=true, it also returns the result of each line you sent, with your externalId and the member it became.
Reading an operation is free, also at a zero balance, and never counted against your rate limits, so you can follow a long import at the pace it asks for. Any key of the workspace with the lists permission can read it.
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/lists/:listId/operations/:operationId/progress.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
listId | string | Yes | List id (UUID) |
operationId | string | Yes | The operationId of the import acknowledgement |
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
wait | integer | No | Hold the request up to this many seconds, 0 to 20, while the operation runs. It answers as soon as the operation settles, pauses or stops, or with the operation still running when the wait ends. Defaults to 0. |
rows | string | No | true adds one page of per-line results |
perPage | integer | No | With rows=true: lines per page, from 1 to 1,000. Defaults to 200. |
cursor | string | No | With rows=true: the rows.nextCursor of the previous page |
cursor and perPage without rows=true return 400 LIST_INVALID_INPUT.
How to follow an operation
Read the operation while clientAction is poll, with wait=20 and an HTTP client timeout above it, for example 30 seconds. Start two reads at least pollAfterSeconds apart (the Retry-After header carries the same value): a held read already lasts longer, so the next one starts right away. Then act on clientAction:
clientAction | What to do |
|---|---|
poll | The operation runs: read it again |
done | It ended. With completed, the data can be read in the List; read rows for the line results |
resume | It paused: stopReason says why and nextStep what to do, see Pauses and how to resume |
reconcile | The outcome is unknown: read the List before doing anything, and never send the import again blindly |
Response structure
| Field | Type | Description |
|---|---|---|
success | boolean | true if the request was processed successfully |
error | null | Always null on success |
dataobjectThe operation
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
operationId | string | Operation id |
listId | string | List id |
kind | string | list_intake_persons or list_intake_companies |
status | string | queued, running, paused, stopped_budget, completed, partial, failed, cancelled, refused or unknown |
clientAction | string | poll, resume, done or reconcile: what to do next |
creditsConsumed | number | Credits charged so far |
creditsPending | number | Credits reserved by steps in flight |
creditsEstimated | number | Credits reserved when the operation was created: 0 for an import, which spends up to maxCredits |
maxCredits | number | null | The ceiling of the operation |
estimatedSeconds | number | null | Seconds left while it runs, 0 once done, null when not known yet |
estimateBlockedUntil | string | null | When the daily extraction limit holds the operation: the next 00:00 UTC, when it resumes |
pollAfterSeconds | number | Seconds to wait before reading again, 0 once you can stop |
pollUrl | string | Path of this endpoint for the operation |
followUp | object | The same follow-up for an agent: tool (get_list_operation, the MCP tool), its arguments (listId, operationId) and pollAfterSeconds |
replay | object | Always { "reuseOperationId": true, "startNewWork": false, "debitAgain": false }: sending the import again with the same operationId starts no new work and charges nothing twice |
stopReason | string | null | Why it is not running: max_credits, balance, blocked_by_paused_operation, paused_by_user (paused from the app), list_member_limit, list_pending_intake_limit, insufficient_credits or cancelled |
errorMessage | string | null | What failed, when it failed |
startedAt | string | null | Start date (ISO 8601) |
completedAt | string | null | End date (ISO 8601), once the data can be read |
createdAt | string | null | Date the operation was created (ISO 8601) |
updatedAt | string | null | Date of its last change (ISO 8601) |
progressobjectLines of the operation
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
total | number | Lines in the operation |
processed | number | Lines processed |
succeeded | number | Lines that found their member and every family asked for |
failed | number | Lines that failed, matched no one, or found their member but not a family asked for (an email not found, for example) |
skipped | number | Lines skipped |
remaining | number | Lines left |
unknownCount | number | Lines whose outcome is unknown |
nextStepobjectWhat to do after a stop, null otherwise
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
action | string | raise_max_credits_and_resume, top_up_and_resume, resume (after a pause from the app), resume_or_cancel_blocking_operation or none |
message | string | The next step, in a sentence you can show |
requiredCredits | number | Worst-case credits of the step that stopped |
minimumMaxCredits | number | After a max_credits stop: the smallest maxCredits that lets it continue |
blockingOperationId | string | The paused operation this one waits for |
rowsobjectWith rows=true, one page of line results; absent without rows=true
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
nextCursor | string | null | Send it back as cursor for the next page, null on the last page |
itemsobject[]One entry per line, in request order
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
index | number | null | Position of the line in signals, or among the non-empty data rows of a sourceUrl file, from 0; null for a file sent from the app |
rowNumber | number | Line number in the operation |
outcome | string | pending (not processed yet), added, already_present, resolved (the member was found, but the line does not say whether it added it: after a restart, or when a requested family could not be bought), not_found, refused, failed, not_reached (not started before a cancel) or unknown |
externalId | string | null | Your identifier, as sent |
memberId | string | null | The member the line names |
errorCode | string | null | What went wrong: for example NOT_FOUND, ENTITY_MISMATCH (a company on a people List, or the reverse), DATA_SUBJECT_BLOCKED (the person asked us to stop processing their data), or CONTACT_EMAIL_NOT_FOUND when the member was found but a family could not be bought |
reason | string | null | How the line ended, once it ended: completed when every requested family was bought, not_found, data_subject_blocked, unknown, or what the family lookup returned. null for a line without families that found its member. |
signal | object | The identifiers the line was sent with |
quotas and metadata are the blocks of every V2 response: see Usage. quotas.creditsConsumed is 0 for the read itself.
An operation that does not belong to this List and workspace returns 404 LIST_OPERATION_NOT_FOUND.
Code examples
Code Examples
curl "https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress?wait=20" \
-H "Authorization: Bearer YOUR_API_KEY"// Once the import is done, read the result of every line.
const KEY = "YOUR_API_KEY";
const pollUrl = "/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress";
const lines = [];
let cursor = null;
do {
const url = new URL(`https://api.reversecontact.com${pollUrl}`);
url.searchParams.set("rows", "true");
url.searchParams.set("perPage", "1000");
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, { headers: { "Authorization": `Bearer ${KEY}` } });
const { data } = await response.json();
lines.push(...data.rows.items);
cursor = data.rows.nextCursor;
} while (cursor);
for (const line of lines) console.log(line.externalId, line.outcome, line.memberId);import requests
url = "https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress"
operation = requests.get(
url,
headers={"Authorization": "Bearer YOUR_API_KEY"},
params={"wait": 20},
timeout=30
).json()["data"]
print(operation["status"], operation["clientAction"])
if operation["clientAction"] == "resume":
print(operation["nextStep"]["message"])Example response
The second example reads the import of Import members and data once done: Jane Doe is added (1 credit) and her email is found (3 credits). John Smith is already on the List, and the email of his Signal becomes his work email, free.
Example Response
{
"success": true,
"data": {
"operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40",
"kind": "list_intake_persons",
"status": "stopped_budget",
"clientAction": "resume",
"progress": {
"processed": 312,
"total": 500,
"succeeded": 298,
"failed": 14,
"skipped": 0,
"remaining": 188,
"unknownCount": 0
},
"creditsConsumed": 998,
"creditsPending": 0,
"creditsEstimated": 0,
"maxCredits": 1000,
"estimatedSeconds": null,
"estimateBlockedUntil": null,
"pollAfterSeconds": 0,
"pollUrl": "/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress",
"followUp": {
"tool": "get_list_operation",
"arguments": {
"listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40",
"operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f"
},
"pollAfterSeconds": 0
},
"replay": { "reuseOperationId": true, "startNewWork": false, "debitAgain": false },
"stopReason": "max_credits",
"nextStep": {
"action": "raise_max_credits_and_resume",
"message": "Paused at the operation's maxCredits ceiling. Resume it with a maxCredits of at least 1001 to continue. Work already done is kept and is not charged again.",
"minimumMaxCredits": 1001,
"requiredCredits": 3
},
"errorMessage": null,
"startedAt": "2026-10-28T09:16:00.000Z",
"completedAt": null,
"createdAt": "2026-10-28T09:15:58.000Z",
"updatedAt": "2026-10-28T09:19:12.000Z"
},
"error": null,
"quotas": {
"creditsConsumed": 0,
"workspace": {
"id": "ws_abc123",
"credits": { "total": 10000, "used": 5520, "left": 4480 }
}
},
"metadata": {
"requestId": "req_o1p2e3r4a5t6",
"executionTimeMs": 26
}
}{
"success": true,
"data": {
"operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40",
"kind": "list_intake_persons",
"status": "completed",
"clientAction": "done",
"progress": {
"processed": 2,
"total": 2,
"succeeded": 2,
"failed": 0,
"skipped": 0,
"remaining": 0,
"unknownCount": 0
},
"creditsConsumed": 4,
"creditsPending": 0,
"creditsEstimated": 0,
"maxCredits": 100,
"estimatedSeconds": 0,
"estimateBlockedUntil": null,
"pollAfterSeconds": 0,
"pollUrl": "/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress",
"followUp": {
"tool": "get_list_operation",
"arguments": {
"listId": "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40",
"operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f"
},
"pollAfterSeconds": 0
},
"replay": { "reuseOperationId": true, "startNewWork": false, "debitAgain": false },
"stopReason": null,
"nextStep": null,
"errorMessage": null,
"startedAt": "2026-10-28T09:16:00.000Z",
"completedAt": "2026-10-28T09:21:38.000Z",
"createdAt": "2026-10-28T09:15:58.000Z",
"updatedAt": "2026-10-28T09:21:38.000Z",
"rows": {
"items": [
{
"index": 0,
"rowNumber": 1,
"outcome": "added",
"externalId": "crm-001",
"memberId": "9b1e4c7a-2d3f-4e8a-b6c5-0a1f2e3d4c5b",
"errorCode": null,
"reason": "completed",
"signal": { "linkedinUrl": "https://www.social.com/in/janedoe" }
},
{
"index": 1,
"rowNumber": 2,
"outcome": "already_present",
"externalId": "crm-002",
"memberId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"errorCode": null,
"reason": "completed",
"signal": { "email": "john.smith@acme.com" }
}
],
"nextCursor": null
}
},
"error": null,
"quotas": {
"creditsConsumed": 0,
"workspace": {
"id": "ws_abc123",
"credits": { "total": 10000, "used": 4526, "left": 5474 }
}
},
"metadata": {
"requestId": "req_o1p2e3r4a5t7",
"executionTimeMs": 31
}
}