> Reverse Contact API documentation. This is the Markdown copy of https://docs.reversecontact.com/endpoints/list-operation
> Every page is listed at https://docs.reversecontact.com/llms.txt, every endpoint with its credit cost and status at https://docs.reversecontact.com/catalog.json

# Read a List operation

> Follow an import with long polling, read why it paused and what to do, and page through its per-line results, for free.

- Endpoint: `GET https://api.reversecontact.com/v2/lists/:listId/operations/:operationId/progress`
- Credits: Free, also at a zero balance, and never counted against the rate limits.
- Answer: synchronous.
- Available in: REST API, MCP tool `get_list_operation`, TypeScript and Python SDKs.
- Last verified: 2026-11-04

## Introduction

The Read a List operation endpoint reads an operation started by an [import](https://docs.reversecontact.com/endpoints/list-intake-signals): 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](https://docs.reversecontact.com/endpoints/list-intake-signals#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                         |

**data** (ListOperation): The operation

| 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](https://docs.reversecontact.com/guides/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)                                                                  |

**progress** (Progress): Lines of the operation

| 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          |

**nextStep** (NextStep): What to do after a stop, `null` otherwise

| 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                                              |

**rows** (LineResults): With `rows=true`, one page of line results; absent without `rows=true`

| Field        | Type           | Description                                                         |
| ------------ | -------------- | ------------------------------------------------------------------- |
| `nextCursor` | string \| null | Send it back as `cursor` for the next page, `null` on the last page |

**items** (LineResult[]): One entry per line, in request order

| 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](https://docs.reversecontact.com/endpoints/usage#response-structure). `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**

```bash [cURL]
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"
```
```javascript [JavaScript]
// 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);
```
```python [Python]
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](https://docs.reversecontact.com/endpoints/list-intake-signals#code-examples) 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**

```json [200 - Paused at maxCredits]
{
  "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": "60a16561-060b-4daa-9838-56deafe39c12",
    "executionTimeMs": 26
  }
}
```
```json [200 - Completed, with rows]
{
  "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": "2d6a1a35-188a-4d9a-868f-2257ee4554c1",
    "executionTimeMs": 31
  }
}
```
