> Reverse Contact API documentation. This is the Markdown copy of https://docs.reversecontact.com/sdk/python/lists-and-bulk
> 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

# Lists and Bulk enrichment

> Manage persistent Lists and run Bulk enrichment in Python, follow operation progress, export CSV results, and reuse identifiers after a restart.

- Last verified: 2026-11-04

From version 0.2.0, `client.lists` manages your [Lists](https://docs.reversecontact.com/guides/lists) and `client.bulk` runs [Bulk enrichment](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) jobs.

`bulk.enrich_persons` starts a job of up to 25,000 people. Use `bulk.wait` to wait until the job is `completed`, `cancelled`, `failed` or `paused`. Once the job is completed or cancelled, `bulk.results` returns the results CSV as text.

The SDK sends an `Idempotency-Key` with each new job, generating one if you omit the key. The SDK reuses the key on its own retries, so a retried request never creates a second job. Items keep the API's camelCase field names.

**Bulk enrichment**

```python [Python SDK]
import os

from reversecontact import ReverseContact

with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client:
    created = client.bulk.enrich_persons(
        name="CRM export, November",
        items=[
            {"linkedinUrl": "https://social.com/in/janedoe", "externalId": "crm-001"},
            {"firstName": "John", "lastName": "Smith", "companyDomain": "acme.com", "externalId": "crm-002"},
        ],
        families=["profile", "contact_email"],
    )
    if created.data is None or created.data.job is None or created.data.job.id is None:
        raise RuntimeError("The response is missing the bulk job id")
    job_id = created.data.job.id

    job = client.bulk.wait(job_id)
    if job.data and job.data.links and job.data.links.results:
        with open("results.csv", "w", encoding="utf-8") as file:
            file.write(client.bulk.results(job_id))
    if job.data:
        print(job.data.status, "credits:", job.data.credits.used if job.data.credits else None)
```

From version 0.3.0, you can use the results before the job settles. `bulk.iter_rows` yields them one at a time, in the order sent, as soon as each one is ready. While the next item runs, it waits out the `Retry-After` of the API. It ends with the job, or when the job is paused: read on later with `bulk.rows` and the last cursor. A cancelled job first finishes the items it had started, within the hour. `bulk.rows` reads one page (`cursor`, `limit` up to 100), with the job status in `extras["status"]`. Each item is a `JobResultLine`, exported by `reversecontact.types`. `bulk.results(job_id, format="ndjson")` returns the results file as NDJSON, one `JobResultLine` per line. The [client.bulk reference](https://docs.reversecontact.com/sdk/python/bulk) documents each method.

**Results as they come**

```python [Python]
import json

for item in client.bulk.iter_rows(job_id):
    print(item.row, item.input.get("externalId"), item.status, item.credits_consumed)

# Or the whole file once the job settles, one JSON object per line:
ndjson = client.bulk.results(job_id, format="ndjson")
items = [json.loads(line) for line in ndjson.splitlines() if line]
```

A List keeps its members. Start an import with `lists.import_signals`, then call `lists.operations.wait`. The helper returns when the operation's `client_action` is `done`, `resume` or `reconcile`, so your code can decide what comes next.

If you omit the identifiers, `lists.create` sends an `Idempotency-Key` and `lists.import_signals` sends an `operation_id`. To resume a paused import after a restart, pass your own `operation_id`.

**Lists**

```python [Python SDK]
import os

from reversecontact import ReverseContact

with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client:
    created = client.lists.create(name="Prospects", entity="persons")
    if created.data is None or created.data.id is None:
        raise RuntimeError("The response is missing the List id")
    list_id = created.data.id

    imported = client.lists.import_signals(
        list_id,
        signals=[{"email": "jane@acme.com", "externalId": "crm-001"}],
        families=["contact_email"],
        max_credits=100,
    )
    if imported.data is None or imported.data.operation_id is None:
        raise RuntimeError("The response is missing the operation id")
    operation = client.lists.operations.wait(list_id, imported.data.operation_id)
    if operation.data:
        print(operation.data.client_action, operation.data.next_step)

    for member in client.lists.members.list_all(list_id):
        print(member)
    csv_text = client.lists.export(list_id)
```

`lists.list_all`, `lists.members.list_all` and `bulk.list_all` read every page for you. They are free, so they need no `max_results`.

## Choose the right wait method

`client.bulk.wait(job_id)` returns when the job leaves `queued` or `running`, including when the job is `failed` or `paused`. Inspect `data.status` to decide what to do next.

`client.lists.operations.wait(list_id, operation_id)` returns when `data.client_action` is no longer `poll`. Inspect `client_action` and `next_step`. Neither helper raises `JobFailedError` for those returned states.

Both helpers accept `timeout` (1800 seconds by default), `poll_wait` (20 by default) and `on_poll`. If the deadline expires, the helper raises `JobTimeoutError` with the job or operation id. For a List operation, resume with `client.lists.operations.wait(list_id, operation_id)`.

For `bulk.list_all()`, pass `per_page=100` explicitly. SDK 0.3.0 otherwise starts from 200 items per page before applying `max_results`, but the API accepts at most 100.

The free `list_all()` iterators accept an optional `max_results` if you want to stop early. Without a cap, the iterators read all pages. The [client.lists reference](https://docs.reversecontact.com/sdk/python/lists) and [client.bulk reference](https://docs.reversecontact.com/sdk/python/bulk) describe each method, cost and return type.

Return to the [Python SDK overview](https://docs.reversecontact.com/sdk/python).

## FAQ

### How many people can I send to Python Bulk enrichment?

`client.bulk.enrich_persons()` accepts up to 25,000 people per job. Each item retains the API camelCase field names, such as `linkedinUrl` and `externalId`.

### Does bulk.wait raise JobFailedError for a failed job?

`client.bulk.wait()` returns failed and paused jobs so your code can inspect `data.status`. The helper raises `JobTimeoutError` if the waiting deadline expires.

### How do I resume a paused List import in Python?

Pass your stored `operation_id` to `client.lists.import_signals()`, then follow progress with `client.lists.operations.wait(list_id, operation_id)`.

### Do List and Bulk iterators need max_results?

The free `lists.list_all()`, `lists.members.list_all()` and `bulk.list_all()` iterators accept an optional `max_results`, with no cap by default.
