PeakAI API Reference
Find verified phone numbers and email addresses from a public professional profile, and search for people and companies.
Base URL: https://build.thepeakai.com
All endpoints below live under /api. JSON in, JSON out.
#Quick start
# 1. sign in (token is short-lived — reuse it, don't sign in per call)
TOKEN=$(curl -s -X POST "https://build.thepeakai.com/token" \
-H "Content-Type: application/json" \
-d '{"id":"you@company.com","password":"YOUR_PASSWORD"}' | jq -r .access_token)
# 2. look up a phone number
curl -X POST "https://build.thepeakai.com/api?type=phone_no&profile_url=https://www.linkedin.com/in/williamhgates" \
-H "Authorization: Bearer $TOKEN"
{
"linkedin_url": "https://www.linkedin.com/in/williamhgates",
"phone_no": ["+1 425 555 0142"],
"from_cache": false,
"credits_charged": 5,
"credits_remaining": 4815
}
#How it works
The LinkedIn profile URL is the key to everything. Give us a URL and ask for what you want — each call is independent, and you only pay for what you ask for.
https://www.linkedin.com/in/janedoe
│
├── type=enrich → full profile: name, headline, current role,
│ experience, education, skills, location
├── type=phone_no → personal / mobile numbers
├── type=email → personal email addresses
└── type=work_email → work email addresses
A typical integration is two steps:
- Enrich —
type=enrichon the URL to get who the person is and where they work. Use that to score, segment or filter your list. - Get contacts — for the ones worth contacting, call
type=phone_noand/ortype=emailon the same URL.
Doing it in that order means you only spend contact credits on profiles that passed your own filtering — enrichment is the cheaper call.
Don't have URLs yet? Use lead search to find people by job title, company, location and so on. It returns profiles you can then enrich and contact exactly as above.
#1. Authentication
Sign in with the credentials issued to you and use the returned token on every request.
POST /token
curl -X POST "https://build.thepeakai.com/token" \
-H "Content-Type: application/json" \
-d '{"id":"you@company.com","password":"YOUR_PASSWORD"}'
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"email": "you@company.com",
"token_expires_in": "900"
}
Send it as a bearer token:
Authorization: Bearer <access_token>
token_expires_in is seconds (returned as a string). Tokens are short-lived
— request a new one when it expires, or refresh shortly before. Don't sign in on
every call; reuse the token until it expires.
Need credentials? Email support@thepeakai.com. API access is enabled per account — if
/apireturns403, that's what's missing.
Long-lived API keys (pk_…) are also supported for server-to-server use and
never expire — create one under API & Integrations in the dashboard, and send
it exactly the same way. Revoke it immediately if it leaks; it carries your
account's full authority.
#2. Credits and pricing
Everything is billed in credits. Prices differ per account (negotiated rates, team plans), so read them at runtime instead of hardcoding:
GET /api/balance
{
"credits": 4820,
"account_type": "user",
"pricing": {
"phone_no": 5,
"email": 1,
"work_email": 1,
"enrich": 1,
"reverse_lookup": 1
},
"plan": "api",
"is_unlimited": false
}
| Action | Cost |
|---|---|
| Phone / email / work email | pricing.<type> — typically 5 for phone, 1 for email |
| Profile enrichment | pricing.enrich |
| Lead or company search | 2 credits per page |
| Export | 0.5 credits per row, minimum 2 |
#You are never charged for
- a lookup that finds nothing
- the same contact again within 28 days (served from cache)
- re-exporting rows you already exported
- any failure on our side or at a data provider (
502,504)
is_unlimited: true means your plan uses daily caps rather than credits; all
prices read 0.
#3. Contact lookup
The core endpoint. One profile, one contact type, one call.
POST /api
| Parameter | Required | Notes |
|---|---|---|
type |
yes | see table below |
profile_url |
for profile types | a linkedin.com/in/... URL |
din |
for din_phone, din_email |
Indian director identification number |
lookupValue |
for reverse_lookup |
an email address or phone number |
Parameters may be sent as query string or JSON body.
type |
Returns |
|---|---|
phone_no |
Personal / mobile numbers |
email |
Personal email addresses |
work_email |
Work email addresses |
enrich |
Full profile — experience, education, skills |
reverse_lookup |
Finds a profile from an email or phone |
din_phone |
Indian company-director phone |
din_email |
Indian company-director email |
Found
{
"linkedin_url": "https://www.linkedin.com/in/williamhgates",
"phone_no": ["+1 425 555 0142"],
"from_cache": false,
"credits_charged": 5,
"credits_remaining": 4815
}
Not found — no charge, and note the field is a string, not an array:
{ "linkedin_url": "https://www.linkedin.com/in/williamhgates", "phone_no": "Not Found" }
Check
Array.isArray(response.phone_no)to distinguish a hit from a miss.
We query several data providers in sequence and return the first verified result, so a call may take a few seconds.
#4. Lead search
Search for people. 2 credits per page, 25 results per page.
POST /api/lead-search
{
"filters": {
"jobTitles": ["Head of Growth"],
"locations": ["India"],
"seniorityLevelIds": ["310"],
"industries": ["4"]
},
"name": "India growth leads",
"page": 1
}
{
"search_id": "7d073fcc-6b7a-4230-bf2e-6972894e180c",
"search_name": "India growth leads",
"page": 1,
"has_more": true,
"total": 8231494,
"count": 25,
"ignored_filters": [],
"credits_charged": 2,
"leads": [
{
"id": "ade75cfb-2cb1-4260-b7a1-9f59a324123e",
"name": "Jane Doe",
"title": "Head of Growth",
"company": "Acme",
"location": "Bengaluru, India",
"linkedin_url": "https://www.linkedin.com/in/...",
"status": "listed"
}
]
}
#Paging
Send the same search_id with the next page number. Pages accumulate under
one search; each costs 2 credits.
{ "filters": { ... }, "page": 2, "search_id": "7d073fcc-..." }
Keep paging while has_more is true. A single search returns up to roughly
2,500 people; narrow the filters if you need more coverage.
#ignored_filters
Names any filter we could not apply — we never silently drop one. If it is non-empty, your results do not reflect those filters.
#Filters
Free text (arrays of strings): search, jobTitles, pastJobTitles,
firstNames, lastNames, companies, pastCompanies, locations,
companyHeadquarterLocations, schools, profileLanguages
IDs — call GET /filter-options (no auth) for valid ids and labels:
industries, seniorityLevelIds, functionIds, yearsOfExperienceIds,
yearsAtCurrentCompanyIds, companyHeadcount
Booleans: recentlyChangedJobs, recentlyPostedOnLinkedIn
Exclusions: every text filter has an exclude* twin —
excludeCurrentJobTitles, excludeLocations, excludeCurrentCompanies,
excludePastCompanies, excludeSchools, excludeIndustryIds,
excludeSeniorityLevelIds, excludeFunctionIds,
excludeCompanyHeadquarterLocations
firstNamesmatches a first name only. Putting a full name there returns nothing — usesearchfor full names.
#Company search
POST /api/company-search — same shape, 50 results per page.
Filters: search, industries, locations (only the first is applied),
headcount.
#5. Revealing contacts on search results
Search returns names and titles, not contacts. To reveal one, call the normal
lookup endpoint with the lead's id:
POST /api?type=phone_no&lead_id=<lead id>
Charged at your normal per-contact rate. The result attaches to the search, so a later export returns it.
You can pass profile_url + search_id instead — the URL from a search
response works directly.
#6. Export
Returns your selected rows including any contacts you have already revealed.
Both ids come from the search response you already have:
search_id is at the top level, and each lead_ids entry is a leads[].id.
Keep them. Store
search_idand the lead ids on your side when you run a search — they are how you export, and how you re-export later. Re-exporting rows you have already exported is free, so storing them costs you nothing and protects you if a file is lost.
POST /api/lead-export
{ "search_id": "7d073fcc-...", "lead_ids": [ "ade75cfb-...", "..."] }
{
"search_id": "7d073fcc-...",
"search_name": "India growth leads",
"credits_charged": 15,
"charged_rows": 30,
"credits_remaining": 4800,
"enriched_count": 0,
"leads": [
{
"id": "ade75cfb-...",
"name": "Jane Doe",
"title": "Head of Growth",
"company": "Acme",
"location": "Bengaluru, India",
"linkedin_url": "https://www.linkedin.com/in/janedoe",
"phone_no": ["+91 90000 00000"],
"email": ["jane@example.com"],
"work_email": null
}
]
}
0.5 credits per row, minimum 2 — charged on new rows only. Rows you already
exported come back free, so losing a file never costs twice. charged_rows
tells you how many were billed.
#Reveal and export in one call
Add enrich_types to reveal contacts for any selected row that lacks them:
{
"search_id": "7d073fcc-...",
"lead_ids": ["..."],
"enrich_types": [ "phone_no", "email"]
}
Contacts are charged at your normal per-contact rate on top of the export fee. Rows already revealed are skipped, never re-charged. If credits run out part-way we stop revealing and still return what succeeded.
POST /api/company-export works the same way.
#7. Errors
Every error returns { "error": "human readable message" }.
| Status | Meaning | Retry? |
|---|---|---|
400 |
Malformed request — see error |
Fix and retry |
401 |
Missing, invalid or revoked key | No |
402 |
Not enough credits | After topping up |
403 |
API access not enabled on this account | Contact support |
404 |
Unknown lead or search | No |
422 |
LinkedIn profile could not be resolved — not charged | Yes, shortly |
429 |
Rate limit or daily cap reached | Later |
502 |
Data provider unavailable — not charged | Yes |
504 |
Data provider timed out — not charged. Usually an over-broad search | Yes, narrow filters |
422, 502 and 504 never charge credits and never consume a daily search.
Retry them safely.
#8. Limits and timeouts
- Lookups: no fixed rate limit on
/api— your credit balance is the constraint. Keep concurrency reasonable (≤10 in flight). - Search: no daily cap on
/api— you pay per page. - Max results: ~2,500 people or ~1,000 companies per search. Narrow the filters and run several searches for broader coverage.
- Timeouts: a lookup takes a few seconds; a search up to 45s when a provider is slow. Set your client timeout to at least 60s.
#9. A complete example
API="https://build.thepeakai.com/api"
# 0. sign in
TOKEN=$(curl -s -X POST "https://build.thepeakai.com/token" \
-H "Content-Type: application/json" \
-d '{"id":"you@company.com","password":"YOUR_PASSWORD"}' | jq -r .access_token)
# 1. search (2 credits)
SEARCH=$(curl -s -X POST "$API/lead-search" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"filters":{"jobTitles":["Head of Growth"],"locations":["India"]},"name":"India growth"}')
SEARCH_ID=$(echo "$SEARCH" | jq -r .search_id)
LEAD_ID=$(echo "$SEARCH" | jq -r .leads[0].id)
# 2. reveal one contact (charged at your phone rate)
curl -s -X POST "$API?type=phone_no&lead_id=$LEAD_ID" -H "Authorization: Bearer $TOKEN"
# 3. export, revealing emails for anything not yet revealed
curl -s -X POST "$API/lead-export" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"search_id\":\"$SEARCH_ID\",\"lead_ids\":[\"$LEAD_ID\"],\"enrich_types\":[\"email\"]}"
#Support
support@thepeakai.com — include the search_id or profile URL and roughly
when the call was made, and we can trace the exact request.