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

# Async jobs

> Wait for live profiles, Find Email, Verify Email and Find Phone jobs in Python, resume polling by id, or parse webhook deliveries from the API.

- Last verified: 2026-11-04

`fetch.person_live`, `fetch.company_live`, `contact.find_email`, `contact.verify_email` and `contact.find_phone` start a job and return a job handle right away. `job.id` is the job's `webhookId`.

Call `job.wait()` to poll the job until it ends. When the job succeeds, the method returns its final state in `data` (`status`, `result`, `credits_consumed`). When the job fails, the method raises `JobFailedError`. Polling is free and does not count toward your rate limit.

**Live fetch**

```python [Python SDK]
import os

from reversecontact import JobFailedError, JobTimeoutError, ReverseContact

with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client:
    job = client.fetch.person_live(url="https://social.com/in/janedoe")
    # Save the id first: after a restart, client.jobs.wait(id) resumes without paying for a new job.
    print("Job id:", job.id)

    try:
        done = job.wait(timeout=1800)
        if done.data is not None and done.data.result is not None:
            print(done.data.result.first_name, "credits:", done.data.credits_consumed)
    except JobFailedError as error:
        print("Job failed:", error.error_code, "credits:", error.credits_consumed)
    except JobTimeoutError as error:
        print("Still running, resume later with client.jobs.wait:", error.job_id)
```

How `wait()` behaves:

- Each poll asks the API to hold the answer for up to 20 seconds (`poll_wait`, 0 to 20), so the result arrives as soon as the job ends. Two polls start at least `Retry-After` seconds apart, 2 seconds when the header is missing.
- `timeout` is the overall deadline in seconds, 1800 (30 minutes) by default. When the deadline passes, `wait()` raises `JobTimeoutError` with `job_id` and `last_status`. The job may still finish. Resume polling with `client.jobs.wait(job_id)`. The synchronous client can exceed the deadline by the length of the poll in progress.
- A job that ends with `status` `errored` raises `JobFailedError`, with `error_code`, `credits_consumed`, `result` and `not_found`. The `not_found` field is `True` for `data-not-found`, `person-not-found`, `company-not-found`, `email-not-found` and `phone-not-found`.
- The SDK calls `on_poll` with each state it reads.
- The SDK never starts a new job after a failure. Your code decides whether to start another job, since the new job can be billed again.

`client.jobs.retrieve(job_id, wait=20)` reads one state of a job without waiting for the end. [Polling](https://docs.reversecontact.com/guides/polling) describes the job lifecycle and how long a result stays available.

### Webhook delivery instead of polling

Pass `webhook_url` when you create the job. The API sends the result to that HTTPS URL as `{webhookId, data}`.

Use `parse_webhook_delivery` to parse the decoded body, a JSON string or bytes. The helper checks the delivery's shape and returns `webhook_id`, `data` and `is_error`. The `is_error` field is `True` when `data.errorCode` is a non-empty string. A malformed delivery raises `ReverseContactError`. The helper does not validate the profile itself.

**Webhook delivery**

```python [Python SDK]
import os

from reversecontact import ReverseContact, parse_webhook_delivery

with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client:
    job = client.fetch.person_live(
        url="https://social.com/in/janedoe",
        webhook_url="https://your-app.com/webhooks/reversecontact",
    )
    print("Store this id to match the delivery:", job.id)

# Call this from the route behind webhook_url with the raw request body.
def handle_webhook(body: bytes) -> None:
    delivery = parse_webhook_delivery(body)
    if delivery.is_error:
        print(delivery.webhook_id, "failed:", delivery.data)
    else:
        print(delivery.webhook_id, "result:", delivery.data)
    # Answer within 10 seconds, then process the data: deduplicate on webhook_id.
```

You can still poll a job created with `webhook_url` by calling `job.wait()`. See [Webhooks](https://docs.reversecontact.com/guides/webhooks) for delivery failures and retries.

## Read one state from a job handle

`job.retrieve(wait=20)` returns one typed `Result[JobSnapshot[T]]`. The profile or contact result keeps the type of the method that created the job. Unlike `wait()`, `retrieve()` returns an `errored` state without raising `JobFailedError`.

**Python SDK**

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

with ReverseContact() as client:
    job = client.contact.find_phone(url="https://social.com/in/janedoe")
    state = job.retrieve(wait=20)
    if state.data:
        print(state.data.status, state.data.result)
```

After a restart, use [client.jobs](https://docs.reversecontact.com/sdk/python/jobs) to retrieve a job or wait for the result by id. `client.jobs.wait(job_id)` returns the endpoint-specific `result` as decoded JSON. The original `Job` handle parses that result into its typed model.

[client.fetch](https://docs.reversecontact.com/sdk/python/fetch) and [client.contact](https://docs.reversecontact.com/sdk/python/contact) list the job-creating methods. Bulk jobs and List operations use their own helpers, described in [Lists and Bulk enrichment](https://docs.reversecontact.com/sdk/python/lists-and-bulk).

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

## FAQ

### How do I wait for a live fetch or Find Email result in Python?

Save `job.id` and call `job.wait()`. Polling is free and waits up to 1800 seconds by default. Resume after a restart with `client.jobs.wait(job_id)`.

### What happens when a Python SDK job fails?

`job.wait()` raises `JobFailedError` for an `errored` job. The exception includes `job_id`, `error_code`, `credits_consumed`, `result` and `not_found`.

### Does JobTimeoutError cancel the enrichment job?

`JobTimeoutError` ends local waiting, without cancelling the remote job. Resume polling with the exception's `job_id`.

### Can I use webhooks and polling together in Python?

Pass `webhook_url` to a live fetch or contact method and parse deliveries with `parse_webhook_delivery`. The same job remains available through `job.wait()`.
