Headcount Dispatch

Home › Endpoints › People Data API Python Example: Search, Find and Enrich

People Data API Python Example: Search, Find and Enrich

Editorial team · Updated 2026-10-14 · 5 min read

A people data API call from Python is an authenticated POST with a small JSON body. Read the key from an environment variable, send the documented fields, check the status code, and treat an empty result as a normal outcome. The example below uses the request fields BusinessMCP documents publicly.

What the documentation gives you, and what it does not

The People Data API page documents three routes, their inputs and their prices. It does not reproduce response schemas, so this article does not invent response field names. Where the code needs to read a result, it passes the parsed JSON through untouched and leaves the field mapping to you, based on the developer docs the page links to.

The base URL is likewise not spelled out on the page as a full URL, only as routes beginning with /api/hub/v1/. The example therefore reads the base URL from a second environment variable, so you can set it from the developer docs.

Call Route Required fields Price
Search /api/hub/v1/people/search domain $0.08 per item
Email finder /api/hub/v1/people/email-finder domain, first_name, last_name $0.04 per call
Enrich /api/hub/v1/people/enrich email or linkedin_url $0.10 per call

Setup

Install the library and set two variables.

pip install requests
export BUSINESSMCP_API_KEY="your-key"
export BUSINESSMCP_BASE_URL="base-url-from-the-developer-docs"

Authentication is a bearer token, as described in RFC 6750. Never commit the key and never ship it to a browser. The account also needs a purchased balance and acceptance of the people-data terms, which include declaring your intended use; the page says both are on the Billing page of the developer portal.

A small client

import os
import requests

BASE = os.environ["BUSINESSMCP_BASE_URL"].rstrip("/")
KEY = os.environ["BUSINESSMCP_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}


def post(path, body, timeout=20):
    resp = requests.post(f"{BASE}{path}", json=body, headers=HEADERS, timeout=timeout)
    resp.raise_for_status()
    return resp.json()


def search_people(domain, seniority=None, department=None, title_contains=None, limit=3):
    body = {"domain": domain, "limit": limit}
    if seniority:
        body["seniority"] = seniority          # list, 0 to 6 items
    if department:
        body["department"] = department        # up to 40 chars
    if title_contains:
        body["title_contains"] = title_contains  # up to 60 chars
    return post("/api/hub/v1/people/search", body)


def find_email(domain, first_name, last_name):
    body = {"domain": domain, "first_name": first_name, "last_name": last_name}
    return post("/api/hub/v1/people/email-finder", body)


def enrich(email=None, linkedin_url=None):
    body = {}
    if email:
        body["email"] = email
    if linkedin_url:
        body["linkedin_url"] = linkedin_url
    return post("/api/hub/v1/people/enrich", body)

The limit default of 3 is our own cap, not a documented default; the page states neither a default nor a maximum for limit.

Key takeaway: validate lengths and required fields in your own code before you call. A malformed request wastes time, and a runaway loop wastes money.

Using it on a list

accounts = ["example.com", "example.org"]
results = {}

for domain in accounts:
    try:
        results[domain] = search_people(
            domain, seniority=["senior"], department="engineering", limit=2
        )
    except requests.HTTPError as err:
        results[domain] = {"error": err.response.status_code}

The seniority value "senior" is a placeholder; check the developer docs for the accepted strings. For background on filter design, read the people search article.

Guards worth adding

  1. Deduplicate inputs. Lowercase and strip domains and emails, then use a set, so you never pay for the same lookup twice in one run.
  2. Cap spend per run. Count calls and stop at a ceiling. Search is billed per item, so also cap limit.
  3. Cache with a timestamp. Save results to disk or a database and skip anything fetched recently.
  4. Handle not-found as normal. Because the page says people calls are charged only when a result is found, an empty response costs nothing. Record it and move on.
  5. Retry carefully. Retry network failures a limited number of times with a delay, but do not retry on a clear client error, since the same request will fail again.
  6. Log the purpose. The provider logs every people call against your account with the declared purpose, and your own logs should state it too.

Our pricing guide shows how to turn call counts into cost per usable contact.

Respecting the rules in code

Some rules belong in your code as much as in your policy documents. The provider never returns people located in the EU, EEA, UK or Switzerland, or people placed in California, Texas, Vermont or Oregon, so a result set for a European account may be empty. Do not work around that by feeding the same people to other sources; see people data and GDPR. Remove opted-out people from your own store promptly, and never use results for credit, employment, insurance or housing decisions.

Testing

Write a test that mocks requests.post and checks your handling of a successful result, an empty result and a server error. Then run a live test against a handful of known accounts using a small balance. If you want to understand the remaining endpoints before you extend the client, the guides to the email finder and enrichment explain the inputs and costs, and the sibling company data API covers account-level lookups with the same style of key.

Where to take the example next

The client above is deliberately minimal. A production version would add structured logging, a persistent cache keyed by domain or email, a spend counter that survives restarts, and a queue so that a long list can be processed in the background. Keep the provider’s rules in configuration rather than scattered through code: the excluded regions, the purpose string and the per-run call ceiling.

For account-level data you can reuse the same pattern. BusinessMCP’s trust and data handling page is worth reading before you store anything long term, since it lists what is and is not retained for several of its tools. Finally, read the guide to choosing a provider if you are still deciding between sources, because the code is easy to port once you know what you are porting to.

Frequently asked questions

How do I call a people data API from Python?

Send an authenticated POST request with the documented JSON fields using the requests library, read the key from an environment variable, and handle empty results and errors.

How do I authenticate?

BusinessMCP uses a bearer token, with its examples reading the key from the BUSINESSMCP_API_KEY environment variable.

What should I do when no result is found?

Treat it as a normal outcome, record not found and continue. BusinessMCP does not charge people calls without a result.

Where are the response fields documented?

BusinessMCP’s People Data API page links to full response references in its developer docs rather than listing them.

Sources