v1

API Documentation

REST API for programmatic people search. Query by name and optional age, city, and country — get a full profile back as JSON.

Overview

The PersonPages API returns the same structured profile you'd get from a normal search on the site. Results are cached, so a repeated query for the same person is served instantly and doesn't cost extra credits.

  • Base URL: https://personpages.com
  • Format: JSON in, JSON out
  • Auth: Bearer token in the Authorization header
  • CORS: Enabled — safe to call from the browser

Authentication

Every request must include your API key as a bearer token. Keys start with pp_live_ (or pp_test_ for sandbox keys) and are shown once at signup — treat them like a password.

Authorization: Bearer pp_live_xxxxxxxxxxxxxxxxxxxxxxxx

Lost your key? You can rotate it from your developer dashboard.

POST /api/public/v1/lookup

Look up a person in two steps, just like on personpages.com. Step 1 — send a name (plus optional age, city, country) and get back a list of matching people (candidates) with a search_id. This step is free. Step 2 — send { search_id, candidate_id } for the person you meant and get the full profile. Only step 2 is billed. Add "direct": true in step 1 to skip the choice and get a single best-guess profile.

Request body

FieldTypeRequiredDescription
namestringYesFull name, 2–120 chars.
ageintegerNoAge, 13–120. Improves match quality.
citystringNoCity name, up to 120 chars.
countrystringNoCountry name, up to 120 chars.

Step 1 response — candidates

json
{
  "ok": true,
  "search_id": "a1b2c3d4e5f6",
  "candidates": [
    {
      "candidate_id": "jane-doe-acme-berlin",
      "name": "Jane Doe",
      "headline": "Product manager at Acme",
      "role": "Product Manager",
      "employer": "Acme",
      "city": "Berlin",
      "country": "Germany",
      "age_estimate": 34,
      "confidence": 87,
      "evidence": ["LinkedIn profile lists Acme, Berlin"],
      "source_urls": ["https://..."],
      "famous": false
    }
  ],
  "next": "POST { search_id, candidate_id } to this endpoint to get the full profile."
}

Step 2 request

json
{ "search_id": "a1b2c3d4e5f6", "candidate_id": "jane-doe-acme-berlin" }

Step 2 response — full profile

json
{
  "ok": true,
  "cached": false,
  "profile_url": "https://personpages.com/profile/jane-doe-berlin",
  "usage": {
    "plan": "pay_as_you_go",
    "free_lookups": 50,
    "used": 12,
    "free_remaining": 38,
    "unbilled_usd": 0,
    "month_spend_usd": 0,
    "resets_at": "2026-08-01T00:00:00.000Z"
  },
  "profile": {
    "slug": "jane-doe-berlin",
    "full_name": "Jane Doe",
    "employer": "...",
    "city": "Berlin",
    "country": "Germany",
    "...": "all fields returned by the profile page"
  }
}

cached: when true, the profile already existed and no new generation ran. Cached lookups count toward your 50 free lookups, and cost $0.05 each after that (new lookups cost $0.39).

Code examples

curl

bash
curl -X POST https://personpages.com/api/public/v1/lookup \
  -H "Authorization: Bearer pp_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jane Doe",
    "age": 34,
    "city": "Berlin",
    "country": "Germany"
  }'

JavaScript (fetch)

javascript
const res = await fetch("https://personpages.com/api/public/v1/lookup", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.PERSONPAGES_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Jane Doe",
    city: "Berlin",
    country: "Germany",
  }),
});
const { search_id, candidates } = await res.json();

// Pick the right person (e.g. show the list to your user)
const pick = candidates[0];
const res2 = await fetch("https://personpages.com/api/public/v1/lookup", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.PERSONPAGES_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ search_id, candidate_id: pick.candidate_id }),
});
const data = await res2.json();
console.log(data.profile_url, data.profile);

Python (requests)

python
import os, requests

r = requests.post(
    "https://personpages.com/api/public/v1/lookup",
    headers={"Authorization": f"Bearer {os.environ['PERSONPAGES_KEY']}"},
    json={"name": "Jane Doe", "city": "Berlin", "country": "Germany"},
    timeout=60,
)
r.raise_for_status()
step1 = r.json()
pick = step1["candidates"][0]  # choose the right person

r = requests.post(
    "https://personpages.com/api/public/v1/lookup",
    headers={"Authorization": f"Bearer {os.environ['PERSONPAGES_KEY']}"},
    json={"search_id": step1["search_id"], "candidate_id": pick["candidate_id"]},
    timeout=90,
)
data = r.json()
print(data["profile_url"], data["profile"])

Errors

Errors are returned as JSON with a machine-readable error code and, where useful, a human message.

StatusCodeMeaning
400invalid_jsonRequest body wasn't valid JSON.
400invalid_inputBody failed validation. See details.
404search_not_foundUnknown search_id or candidate_id.
401missing_api_keyNo Authorization header.
401invalid_api_keyKey not recognized.
403api_key_disabledKey exists but is deactivated.
402payment_failedYour card was declined. Update it on the developer page.
429free_quota_exceeded50 free lookups used and no card on file.
502generation_failedUpstream provider error. Safe to retry.
500server_errorUnexpected server error.

Quotas & billing

  • Every key gets 50 free lookups per month (person or website).
  • After that: $0.39 per new lookup, $0.05 per cached lookup.
  • Your saved card is charged daily for the previous usage. If a charge fails, the key returns 402 until the card works again.
  • No spending cap. Failed calls (4xx / 5xx) are never billed.
  • Free lookups reset on the 1st of each month, UTC.
  • Check your live balance and usage in the developer dashboard.
  • Need volume pricing? Talk to us about Enterprise.

FAQ

Does the API return the same data as the website?

Yes. The same profile object powers both the public profile page and the API response.

Are new profiles created when I use the API?

Yes. If the person isn't in our index, the API triggers the same generation pipeline as a normal search, and a public profile page is created at the returned profile_url.

Is there a sandbox / test environment?

Test-mode keys start with pp_test_ and are issued when you sign up with a Stripe test card. They hit the same endpoint and behave identically.

Rate limits?

If you need burst-safe rate limits or an SLA, contact us.

Questions the docs don't answer? Get in touch.