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

# Python

> Call the Reverse Contact API from Python with a small requests helper, and generate typed Pydantic models from the OpenAPI spec.

- Last verified: 2026-10-15

## Introduction

There is no Reverse Contact package to install. The API is JSON over HTTPS, so [requests](https://requests.readthedocs.io) and the helper below are all a call needs. For typed request and response objects, generate Pydantic models from the OpenAPI spec this site publishes at `https://docs.reversecontact.com/openapi.json`, which is updated with each docs release.

Keep the API key out of your code, in an environment variable such as `RC_API_KEY`: see [Authentication](https://docs.reversecontact.com/authentication).

## A small client

The helper sends one request and follows the retry rule of [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries): on a `429`, wait the number of seconds in `error.details.retryAfter`, then retry; on a `500`, `502`, `503` or `504`, retry with exponential backoff, a few times at most. It reads the status before the body, because a `502` or `503` from the network edge may not be JSON. A `429` with the code `DAILY_LIMIT_EXCEEDED` is the daily limit: it is not retried, since it does not clear within minutes. The helper returns the HTTP status and the response envelope (`success`, `data`, `error`, and `quotas` and `metadata` when the response carries them).

**rc_client.py**

```python [Python]
import os
import time

import requests

API = "https://api.reversecontact.com/v2"
BACKOFF = {500, 502, 503, 504}
MAX_ATTEMPTS = 5

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['RC_API_KEY']}"

def rc(path, body=None):
    """One call to the API. 429: waits error.details.retryAfter. 500, 502, 503, 504: backs off."""
    for attempt in range(1, MAX_ATTEMPTS + 1):
        if body is None:
            response = session.get(f"{API}{path}", timeout=30)
        else:
            response = session.post(f"{API}{path}", json=body, timeout=30)

        # The status first: a 502 or 503 from the network edge may not be JSON.
        if response.status_code in BACKOFF and attempt < MAX_ATTEMPTS:
            time.sleep(min(2**attempt, 15))
            continue
        try:
            payload = response.json()
        except ValueError:
            payload = {"success": False, "error": {"code": f"HTTP_{response.status_code}", "message": "The response is not JSON"}}

        error = payload.get("error") or {}
        if response.status_code == 429 and error.get("code") != "DAILY_LIMIT_EXCEEDED" and attempt < MAX_ATTEMPTS:
            time.sleep((error.get("details") or {}).get("retryAfter", 60))
            continue
        return response.status_code, payload
```

## Enrich a person

**Enrich a person**

```python [Python]
from rc_client import rc

# 2 credits when a person is found, a 404 is free.
status, result = rc("/enrich/persons", {"email": "jane.doe@acme.com"})

if result["success"]:
    person = result["data"]["person"]
    print(person["firstName"], person["lastName"], person["currentPositionTitle"])
    print("Credits used:", result["quotas"]["creditsConsumed"])
elif status == 404:
    print("No confident match: nothing was charged")
else:
    print(status, result["error"]["code"], result["error"]["message"])
```

On Enrich, a `404` is not an error to retry: it means no profile matched with confidence, and it costs nothing. What a `404` means depends on the endpoint: on a cached fetch, a profile that is not in the cache; on `/v2/webhooks`, an unknown job. Every error other than the retried ones needs a change to the request or the account before it can succeed.

## Async jobs

The live fetch endpoints, [Find Email](https://docs.reversecontact.com/endpoints/contact-email) and [Find Phone](https://docs.reversecontact.com/endpoints/contact-phone) answer with a job. Poll it until it succeeds or fails: polling is free and does not count toward your rate limit. Give the loop a maximum duration, and stop on a poll that answers an error: `404 WEBHOOK_EVENT_NOT_FOUND` for an unknown job, `410 WEBHOOK_EVENT_EXPIRED` for a result older than 72 hours.

**Live fetch and polling**

```python [Python]
import time

from rc_client import rc

status, job = rc("/fetch/persons/live", {"url": "https://social.com/in/janedoe"})
if not job["success"]:
    raise RuntimeError(f"{status} {job['error']['code']}: {job['error']['message']}")
webhook_id = job["data"]["webhookId"]

# Results usually arrive within seconds, but can take tens of minutes.
deadline = time.monotonic() + 30 * 60

while True:
    if time.monotonic() > deadline:
        raise TimeoutError(f"Job {webhook_id} still running: poll it again later")
    time.sleep(5)
    status, poll = rc(f"/webhooks/{webhook_id}")
    # 404 and 410 mean there is nothing left to wait for.
    if not poll["success"]:
        raise RuntimeError(f"{status} {poll['error']['code']}: {poll['error']['message']}")
    if poll["data"]["status"] == "succeeded":
        print(poll["data"]["result"])
        break
    if poll["data"]["status"] == "errored":
        raise RuntimeError(poll["data"]["errorCode"])
```

Every 5 to 10 seconds suits a background job. See [Polling](https://docs.reversecontact.com/guides/polling) for the intervals that fit other cases, and for how long a result stays available.

## Typed models from the OpenAPI spec

[datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator) turns the spec into Pydantic models, one per request body and response object. The spec declares email fields, so the models need the `email` extra of Pydantic.

The spec closes its objects (`additionalProperties: false`), so by default every generated model refuses fields it does not know, and a field the API adds later would make every response fail validation. `--extra-fields ignore` keeps the models working when the API grows.

**Generate the models**

```bash [pip]
pip install "datamodel-code-generator[http]" "pydantic[email]"
datamodel-codegen --url https://docs.reversecontact.com/openapi.json \
  --input-file-type openapi \
  --output-model-type pydantic_v2.BaseModel \
  --extra-fields ignore \
  --output reversecontact_models.py
```

Run it again whenever the [changelog](https://docs.reversecontact.com/changelog) announces a change to an endpoint you call. The models validate what you send and what you read:

**Typed request and response**

```python [Python]
from rc_client import rc
from reversecontact_models import EnrichPersonBody, EnrichPersonData

body = EnrichPersonBody(email="jane.doe@acme.com")
status, result = rc("/enrich/persons", body.model_dump(exclude_none=True))

if result["success"]:
    data = EnrichPersonData.model_validate(result["data"])
    print(data.person.firstName, data.person.currentPositionTitle, data.isFullProfile)
```

## FAQ

### Is there an official Reverse Contact Python package?

No. The Reverse Contact API is JSON over HTTPS, so `requests` and a small helper are enough, and `datamodel-code-generator` builds typed Pydantic models from `https://docs.reversecontact.com/openapi.json`.

### How should a Python script handle rate limits on the Reverse Contact API?

On a `429`, wait the number of seconds in `error.details.retryAfter` and send the request again, except for `DAILY_LIMIT_EXCEEDED`, the daily limit. Retry `500`, `502`, `503` and `504` with exponential backoff, a few times at most, and do not retry other errors without changing the request.

### Why do the generated Reverse Contact models need `pydantic[email]`?

The Reverse Contact OpenAPI spec declares email fields, such as `email` on `EnrichPersonBody`, which Pydantic validates with its optional `email` extra. Install `pydantic[email]` alongside the models.
