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

# client.bulk

> Start, monitor and cancel Bulk enrichment jobs with the Python SDK, read results as they come, or download them as CSV or NDJSON.

- Last verified: 2026-11-04

Start Bulk enrichment jobs, follow progress, browse job history, read results as they come and download them.

| Method | Endpoint | Cost |
| ------ | -------- | ---- |
| [`enrich_persons()`](#enrich_persons) | [POST /v2/enrich/persons/bulk](https://docs.reversecontact.com/endpoints/bulk-enrich-persons) | Free to send. Each item costs the same as its individual call. Credits are reserved when the job is created and refunded for what is not found. |
| [`list()`](#list) | [GET /v2/jobs](https://docs.reversecontact.com/endpoints/jobs) | Free. |
| [`list_all()`](#list_all) | [GET /v2/jobs](https://docs.reversecontact.com/endpoints/jobs) | Free. |
| [`retrieve()`](#retrieve) | [GET /v2/jobs/{jobId}](https://docs.reversecontact.com/endpoints/job) | Free, even at a zero balance. Never counts toward rate limits. |
| [`results()`](#results) | [GET /v2/jobs/{jobId}/results](https://docs.reversecontact.com/endpoints/job-results) | Free, even at a zero balance. |
| [`rows()`](#rows) | [GET /v2/jobs/{jobId}/rows](https://docs.reversecontact.com/endpoints/job-rows) | Free, even at a zero balance. Each page counts toward rate limits. |
| [`iter_rows()`](#iter_rows) | [GET /v2/jobs/{jobId}/rows](https://docs.reversecontact.com/endpoints/job-rows) | Free, even at a zero balance. Each page counts toward rate limits. |
| [`cancel()`](#cancel) | [POST /v2/jobs/{jobId}/cancel](https://docs.reversecontact.com/endpoints/job-cancel) | Free. Items that had not started are never charged. |
| [`wait()`](#wait) | [GET /v2/jobs/{jobId}](https://docs.reversecontact.com/endpoints/job) | Free, even at a zero balance. Never counts toward rate limits. |

## Calling conventions

Examples assume `RC_API_KEY` is set. Replace example ids with ids returned by your own requests. Signatures show the published 0.3.0 synchronous methods. Use the same arguments with the [Python async client](https://docs.reversecontact.com/sdk/python/async-client).

`models` refers to `reversecontact.types`. `NOT_GIVEN` and `NotGiven` are the SDK's internal markers for omitted arguments. Leave these arguments out of your calls. For API body parameters, passing `None` sends an explicit null. Transport options use `None` to inherit their defaults. `Timeout` means a number of seconds or `httpx.Timeout`. Supply parameters marked **Required** to Python or to the API, even when the generated signature permits omission.

JSON response objects expose `data`, `quotas`, `metadata` and `http_response`. Check optional fields for `None`. See [First call](https://docs.reversecontact.com/sdk/python/first-call) for the response envelope. CSV methods return text and iterators yield items.

HTTP failures raise an `APIError` subclass based on the response: `BadRequestError`, `AuthenticationError`, `InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `GoneError`, `UnprocessableEntityError`, `RateLimitError`, `DailyLimitError`, `KeyCreditLimitError`, `UnavailableForLegalReasonsError` or `InternalServerError`. See the [error mapping and retry rules](https://docs.reversecontact.com/sdk/python/errors-and-retries).

## enrich_persons

Send 1 to 25,000 items. Nested item dictionaries use the API names, such as `linkedinUrl`, `firstName` and `externalId`.

**Python SDK**

```python [Python SDK]
def enrich_persons(
    *,
    items: list[models.BulkEnrichPersonsParamsItemsItem | dict[str, object]] | NotGiven | None = NOT_GIVEN,
    families: list[str] | NotGiven | None = NOT_GIVEN,
    email_type: str | NotGiven | None = NOT_GIVEN,
    live: bool | NotGiven | None = NOT_GIVEN,
    name: str | NotGiven | None = NOT_GIVEN,
    webhook_url: str | NotGiven | None = NOT_GIVEN,
    timeout: Timeout | None = None,
    max_retries: int | None = None,
    headers: Mapping[str, str] | None = None,
    idempotency_key: str | None = None,
    **extra: object,
) -> Result[models.BulkEnrichPersonsData]: ...
```

**Parameters**

| Name | Type | Description |
| ---- | ---- | ----------- |
| `items` | `list[models.BulkEnrichPersonsParamsItemsItem \| dict[str, object]] \| NotGiven \| None` | **Required.** 1 to 25,000 people, see [Items](https://docs.reversecontact.com/endpoints/bulk-enrich-persons#items) |
| `families` | `list[str] \| NotGiven \| None` | Optional. What to look up for each item: `profile`, `contact_email`, `contact_phone`. Defaults to `["profile"]`. |
| `email_type` | `str \| NotGiven \| None` | Optional. With `contact_email`, the kind of email to find: `professional` (default), `personal` or `any` |
| `live` | `bool \| NotGiven \| None` | Optional. `true` fetches the profile of a Social profile URL item live. Omitted or `false`, saved data is used. |
| `name` | `str \| NotGiven \| None` | Optional. A name for the job, up to 255 characters, shown in the job history and in the app. Defaults to `Sent via API`. |
| `webhook_url` | `str \| NotGiven \| None` | Optional. Public HTTPS URL that receives one `POST` when the job settles. See [Webhook callback](https://docs.reversecontact.com/endpoints/bulk-enrich-persons#webhook-callback) |
| `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. |
| `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. |
| `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. |
| `idempotency_key` | `str \| None` | Optional. Key for this creation request. The SDK generates a key when omitted and reuses it on automatic retries. |
| `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. |

**Returns**

`Result[models.BulkEnrichPersonsData]`. `data.job.id` identifies the Bulk job. The acknowledgement also contains `duplicate_count`, `credits_estimated` and `not_run_for_credits`. Follow the job with `client.bulk.wait(job_id)`. See [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/python/lists-and-bulk).

**Errors**

`APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`.

**Example**

**Python SDK**

```python [Python SDK]
from reversecontact import ReverseContact

with ReverseContact() as client:
    result = client.bulk.enrich_persons(
        items=[{"linkedinUrl": "https://social.com/in/janedoe", "externalId": "crm-001"}],
        families=["profile"],
        name="CRM enrichment",
    )
    print(result.data if result is not None else None)
```
## list

Call [Job history](https://docs.reversecontact.com/endpoints/jobs) with `client.bulk.list()`.

**Python SDK**

```python [Python SDK]
def list(
    *,
    type: str | NotGiven | None = NOT_GIVEN,
    status: str | NotGiven | None = NOT_GIVEN,
    list_id: str | NotGiven | None = NOT_GIVEN,
    per_page: int | NotGiven | None = NOT_GIVEN,
    cursor: str | NotGiven | None = NOT_GIVEN,
    timeout: Timeout | None = None,
    max_retries: int | None = None,
    headers: Mapping[str, str] | None = None,
    **extra: object,
) -> Page[models.ListJobsItem]: ...
```

**Parameters**

| Name | Type | Description |
| ---- | ---- | ----------- |
| `type` | `str \| NotGiven \| None` | Optional. Filter by these comma-separated types. See [Job types](https://docs.reversecontact.com/endpoints/jobs#job-types) |
| `status` | `str \| NotGiven \| None` | Optional. Filter by these comma-separated statuses: `queued`, `running`, `paused`, `completed`, `cancelled`, `failed` |
| `list_id` | `str \| NotGiven \| None` | Optional. Return only jobs for this List (UUID) |
| `per_page` | `int \| NotGiven \| None` | Optional. Jobs per page, from 1 to 100. Defaults to 50. |
| `cursor` | `str \| NotGiven \| None` | Optional. The `nextCursor` of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns `400`. |
| `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. |
| `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. |
| `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. |
| `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. |

**Returns**

`Page[models.ListJobsItem]`. A page with records in `data`, a `next_cursor`, `has_next_page()` and `get_next_page()`. The next-page method reuses the filters and returns `None` after the last page.

**Errors**

`APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`.

**Example**

**Python SDK**

```python [Python SDK]
from reversecontact import ReverseContact

with ReverseContact() as client:
    page = client.bulk.list(per_page=20)
    for item in page.data:
        if item is not None:
            print(item.to_dict())
```
## list_all

Read every job-history page with `client.bulk.list_all(per_page=100)`. In SDK 0.3.0, omitting `per_page` starts the iterator at a page size of 200, reduced by any remaining `max_results` cap. The API accepts at most 100 per page, so set `per_page` explicitly to a value from 1 to 100.

**Python SDK**

```python [Python SDK]
def list_all(
    *,
    type: str | NotGiven | None = NOT_GIVEN,
    status: str | NotGiven | None = NOT_GIVEN,
    list_id: str | NotGiven | None = NOT_GIVEN,
    per_page: int | NotGiven | None = NOT_GIVEN,
    cursor: str | NotGiven | None = NOT_GIVEN,
    timeout: Timeout | None = None,
    max_retries: int | None = None,
    headers: Mapping[str, str] | None = None,
    max_results: int | None = None,
    **extra: object,
) -> Iterator[models.ListJobsItem | None]: ...
```

**Parameters**

| Name | Type | Description |
| ---- | ---- | ----------- |
| `type` | `str \| NotGiven \| None` | Optional. Filter by these comma-separated types. See [Job types](https://docs.reversecontact.com/endpoints/jobs#job-types) |
| `status` | `str \| NotGiven \| None` | Optional. Filter by these comma-separated statuses: `queued`, `running`, `paused`, `completed`, `cancelled`, `failed` |
| `list_id` | `str \| NotGiven \| None` | Optional. Return only jobs for this List (UUID) |
| `per_page` | `int \| NotGiven \| None` | Optional in the signature. Set 1 to 100 explicitly: SDK 0.3.0 otherwise starts from 200 before applying `max_results`, above the API maximum of 100. |
| `cursor` | `str \| NotGiven \| None` | Optional. The `nextCursor` of the previous page, sent with the same filters. A malformed cursor, or one sent with other filters, returns `400`. |
| `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. |
| `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. |
| `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. |
| `max_results` | `int \| None` | Optional. Maximum items yielded. `None` reads every page. `0` makes no request. |
| `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. |

**Returns**

`Iterator[models.ListJobsItem | None]`. An iterator of typed records. Entries can be `None`. The iterator fetches pages only as you advance. Pagination is free. Omit `max_results` to read all pages.

**Errors**

`APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options, invalid `max_results` or `per_page`, or a repeated pagination cursor. Malformed typed response data can raise Pydantic `ValidationError`.

**Example**

**Python SDK**

```python [Python SDK]
from reversecontact import ReverseContact

with ReverseContact() as client:
    for item in client.bulk.list_all(per_page=100, max_results=20):
        if item is not None:
            print(item.to_dict())
```
## retrieve

Call [Get a job](https://docs.reversecontact.com/endpoints/job) with `client.bulk.retrieve()`.

**Python SDK**

```python [Python SDK]
def retrieve(
    job_id: str,
    *,
    wait: int | NotGiven | None = NOT_GIVEN,
    timeout: Timeout | None = None,
    max_retries: int | None = None,
    headers: Mapping[str, str] | None = None,
    **extra: object,
) -> Result[models.GetJobData]: ...
```

**Parameters**

| Name | Type | Description |
| ---- | ---- | ----------- |
| `job_id` | `str` | **Required.** Bulk job id (UUID). |
| `wait` | `int \| NotGiven \| None` | Optional. Hold the request up to this many seconds, 0 to 20, while the job is queued or running. It answers as soon as the job settles or pauses, or with the job still running when the wait ends. Defaults to 0. |
| `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. |
| `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. |
| `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. |
| `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. |

**Returns**

`Result[models.GetJobData]`. `data` contains one job state, including `status`, `progress`, `credits`, `error` and `links`.

**Errors**

`APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options or a `wait` outside the integer range 0 to 20. Malformed typed response data can raise Pydantic `ValidationError`.

**Example**

**Python SDK**

```python [Python SDK]
from reversecontact import ReverseContact

with ReverseContact() as client:
    result = client.bulk.retrieve("00000000-0000-4000-8000-000000000001", wait=20)
    print(result.data if result is not None else None)
```
## results

Results are available once the job is completed or cancelled, as CSV (the default) or with `format="ndjson"` as one `JobResultLine` JSON object per line.

**Python SDK**

```python [Python SDK]
def results(
    job_id: str,
    *,
    format: str | NotGiven | None = NOT_GIVEN,
    timeout: Timeout | None = None,
    max_retries: int | None = None,
    headers: Mapping[str, str] | None = None,
    **extra: object,
) -> str: ...
```

**Parameters**

| Name | Type | Description |
| ---- | ---- | ----------- |
| `job_id` | `str` | **Required.** Bulk job id (UUID). |
| `format` | `str \| NotGiven \| None` | Optional. `csv` or `ndjson`. Defaults to `csv`. |
| `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. |
| `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. |
| `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. |
| `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. |

**Returns**

`str`. The completed or cancelled job's results as text, one row per item in input order: the CSV, or NDJSON with `format="ndjson"`.

**Errors**

`APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. `ConflictError` with `JOB_RESULTS_NOT_READY` means the file is not ready. `ConflictError` with `JOB_RESULTS_FORMAT_UNAVAILABLE` means the job was stored before NDJSON existed: download its CSV.

**Example**

**Python SDK**

```python [Python SDK]
import json

from reversecontact import ReverseContact

with ReverseContact() as client:
    job_id = "00000000-0000-4000-8000-000000000001"
    csv_text = client.bulk.results(job_id)
    ndjson = client.bulk.results(job_id, format="ndjson")
    items = [json.loads(line) for line in ndjson.splitlines() if line]
    print(len(csv_text), len(items))
```

## rows

Read one page of results, in the order sent, from the start of the run. While the job runs, a page stops at the first item not finished: `data` is empty and `next_cursor` is set while the next item runs, so read it again after `Retry-After`. `next_cursor` is `None` once nothing more will come. Available from SDK 0.3.0.

**Python SDK**

```python [Python SDK]
def rows(
    job_id: str,
    *,
    cursor: str | NotGiven | None = NOT_GIVEN,
    limit: int | NotGiven | None = NOT_GIVEN,
    timeout: Timeout | None = None,
    max_retries: int | None = None,
    headers: Mapping[str, str] | None = None,
    **extra: object,
) -> Page[models.JobResultLine]: ...
```

**Parameters**

| Name | Type | Description |
| ---- | ---- | ----------- |
| `job_id` | `str` | **Required.** Bulk job id (UUID). |
| `cursor` | `str \| NotGiven \| None` | Optional. `next_cursor` of the previous page, up to 512 characters. Without it, the page starts at the first item. |
| `limit` | `int \| NotGiven \| None` | Optional. Items per page, 1 to 100. Defaults to 100. |
| `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. |
| `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. |
| `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. |
| `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. |

**Returns**

`Page[models.JobResultLine]`, with the items in `data`, `next_cursor`, `has_next_page()` and `get_next_page()`. `extras["status"]` is the job status when the page was read. A page can hold fewer items than `limit`.

**Errors**

`APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. `ConflictError` with `JOB_RESULTS_FORMAT_UNAVAILABLE` means the job was stored before reading by page existed: download its CSV with `results()`. `RateLimitError` with `JOB_ROWS_BUSY` means too many pages are being read at once: the SDK retries it as the [retry rules](https://docs.reversecontact.com/sdk/python/errors-and-retries) describe.

**Example**

**Python SDK**

```python [Python SDK]
from reversecontact import ReverseContact

with ReverseContact() as client:
    page = client.bulk.rows("00000000-0000-4000-8000-000000000001", limit=100)
    for item in page.data:
        print(item.row, item.status)
    print(page.extras["status"], page.next_cursor)
```

## iter_rows

Read the results one item at a time, in the order sent, as they become ready, whatever the job is doing. While the next item runs, the iterator waits `Retry-After` (2 seconds by default) and reads the same cursor again. It ends when `next_cursor` is `None`, or when the job is paused, since nothing moves until someone resumes it: read on later with `rows()` and the last cursor. A cancelled job still finishes the items it had started, within the hour, and the iterator waits for them too. Available from SDK 0.3.0. The async client has the same method, used with `async for`.

**Python SDK**

```python [Python SDK]
def iter_rows(
    job_id: str,
    *,
    cursor: str | None = None,
    limit: int | None = None,
    timeout: Timeout | None = None,
    max_retries: int | None = None,
    headers: Mapping[str, str] | None = None,
) -> Iterator[models.JobResultLine | None]: ...
```

**Parameters**

| Name | Type | Description |
| ---- | ---- | ----------- |
| `job_id` | `str` | **Required.** Bulk job id (UUID). |
| `cursor` | `str \| None` | Optional. Start after a page you already read. |
| `limit` | `int \| None` | Optional. Items per page, 1 to 100. |
| `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. |
| `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. |
| `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. |

**Returns**

An iterator of `models.JobResultLine` items, in the order sent.

**Errors**

The errors of `rows()`.

**Example**

**Python SDK**

```python [Python SDK]
from reversecontact import ReverseContact

with ReverseContact() as client:
    for item in client.bulk.iter_rows("00000000-0000-4000-8000-000000000001"):
        print(item.row, item.input.get("externalId"), item.status, item.credits_consumed)
```
## cancel

Call [Cancel a job](https://docs.reversecontact.com/endpoints/job-cancel) with `client.bulk.cancel()`.

**Python SDK**

```python [Python SDK]
def cancel(
    job_id: str,
    *,
    timeout: Timeout | None = None,
    max_retries: int | None = None,
    headers: Mapping[str, str] | None = None,
    **extra: object,
) -> Result[models.CancelJobData]: ...
```

**Parameters**

| Name | Type | Description |
| ---- | ---- | ----------- |
| `job_id` | `str` | **Required.** Bulk job id (UUID). |
| `timeout` | `Timeout \| None` | Optional. Seconds per HTTP attempt, or `httpx.Timeout`. Uses the client setting when `None`. |
| `max_retries` | `int \| None` | Optional. Retries after the first HTTP attempt. Uses the client setting when `None`. |
| `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. |
| `**extra` | `object` | Optional. Additional API keyword arguments, including camelCase aliases, passed through to the API. |

**Returns**

`Result[models.CancelJobData]`. `data` contains the job state after cancellation, including `status`, `progress` and `credits`.

**Errors**

`APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid SDK options. Malformed typed response data can raise Pydantic `ValidationError`.

**Example**

**Python SDK**

```python [Python SDK]
from reversecontact import ReverseContact

with ReverseContact() as client:
    result = client.bulk.cancel("00000000-0000-4000-8000-000000000001")
    print(result.data if result is not None else None)
```
## wait

Call [Get a job](https://docs.reversecontact.com/endpoints/job) with `client.bulk.wait()`.

**Python SDK**

```python [Python SDK]
def wait(
    job_id: str,
    *,
    timeout: float = 1800,
    poll_wait: int = 20,
    on_poll: Callable[[Result[GetJobData]], None] | None = None,
    headers: Mapping[str, str] | None = None,
) -> Result[GetJobData]: ...
```

**Parameters**

| Name | Type | Description |
| ---- | ---- | ----------- |
| `job_id` | `str` | **Required.** Bulk job id (UUID). |
| `timeout` | `float` | Optional. Overall polling deadline in seconds. Default `1800`. |
| `poll_wait` | `int` | Optional. Long-poll duration from `0` to `20` seconds. Default `20`. |
| `on_poll` | `Callable[[Result[GetJobData]], None] \| None` | Optional. Synchronous callback called with each successful poll response. |
| `headers` | `Mapping[str, str] \| None` | Optional. Additional HTTP headers for this call. |

**Returns**

`Result[GetJobData]`. `data` contains the job when the status leaves `queued` or `running`, including `completed`, `cancelled`, `failed` or `paused`. Inspect `status`. Failed and paused jobs are returned without `JobFailedError`.

**Errors**

`APIError` subclasses follow the HTTP mapping above. `APIConnectionError` reports transport failures. `APITimeoutError` reports timed-out attempts. `ReverseContactError` covers invalid `poll_wait` or `timeout`. `JobTimeoutError` reports an expired deadline. Failed or paused server states are returned for inspection. Malformed typed response data can raise Pydantic `ValidationError`.

**Example**

**Python SDK**

```python [Python SDK]
from reversecontact import ReverseContact

with ReverseContact() as client:
    result = client.bulk.wait("00000000-0000-4000-8000-000000000001")
    print(result.data if result is not None else None)
```
## Related guides

See the [Python SDK overview](https://docs.reversecontact.com/sdk/python), [Lists and bulk](https://docs.reversecontact.com/sdk/python/lists-and-bulk) and [Lists](https://docs.reversecontact.com/sdk/python/lists).

## FAQ

### Does bulk.enrich_persons return a Job handle?

`client.bulk.enrich_persons()` returns a `Result` acknowledgement. Read `data.job.id`, then call `client.bulk.wait(job_id)`.

### Does bulk.wait raise when the Bulk job fails?

`client.bulk.wait()` returns completed, cancelled, failed or paused job states. Inspect `data.status`. If the wait exceeds its deadline, the SDK raises `JobTimeoutError`.

### How do I download Bulk enrichment results in Python?

Once the job is completed or cancelled, call `client.bulk.results(job_id)` to download the CSV as text, or `client.bulk.results(job_id, format="ndjson")` for NDJSON. From SDK 0.3.0, `client.bulk.iter_rows(job_id)` reads each result as soon as it is ready.

### How do I list all Bulk jobs in Python?

Use the free `client.bulk.list_all(per_page=100)` iterator. With SDK 0.3.0, set `per_page` explicitly. `max_results` is optional and limits how many job records the iterator yields.
