Skip to content

Python

New

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

Last verified

Introduction

There is no Reverse Contact package to install. The API is JSON over HTTPS, so requests 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.

A small client

The helper sends one request and follows the retry rule of Error handling & 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).

Python

rc_client.py

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

Python

Enrich a person

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 and Find 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.

Python

Live fetch and polling

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 for the intervals that fit other cases, and for how long a result stays available.

Typed models from the OpenAPI spec

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.

pip

Generate the models

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 announces a change to an endpoint you call. The models validate what you send and what you read:

Python

Typed request and response

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.