People Finder API
CluGTM’s People Finder API finds B2B contacts by structured filters and charges 0 credits to do it. POST /api/v2/people/find accepts 17 filter keys covering job title, seniority, department, location, education, skills and employer firmographics, returns matched people with LinkedIn URL and company, and hands back a task_id you can pass straight into enrichment. You are charged only when you turn a match into a verified email (1 credit), a direct-dial phone (10 credits) or both (11 credits).
Audience: developers. API keys need a Pro or Enterprise plan (legacy Pro and Scale plans included) or an AppSumo Tier 5, 6 or 7 license, and they never work during a trial. Search itself is free, but you need API access to call it. To search from Claude instead, use the CluGTM MCP.
How do I call the CluGTM People Finder API?
POST https://api.cleanlist.ai/api/v2/people/find with a clapi_ Bearer token and the people:read scope. The body takes a filters object (or the structured criteria form), an optional limit (default 25) and an optional cursor.
cURL
curl -X POST https://api.cleanlist.ai/api/v2/people/find \
-H "Authorization: Bearer clapi_your_key" \
-H "Content-Type: application/json" \
-d '{
"filters": {
"management_levels": ["VP"],
"departments": ["Sales"],
"location_country": ["Canada"],
"company_headcount": ["51-200"]
},
"limit": 25
}'What filters does the People Finder API accept?
Filters AND across keys and OR within each array. These 17 keys are the complete flat-filter surface:
| Key | Example | Notes |
|---|---|---|
titles | ["VP Sales"] | Exact, case-sensitive match on the current title. Under-returns badly; see below |
seniority | ["Founder", "C-Suite"] | Fixed values: Owner, Founder, C-Suite, Partner, Vice President, Head, Director, Manager, Senior, Entry, Trainee, Unpaid |
management_levels | ["VP", "Director"] | Fixed values: C-Suite, VP, Director, Manager |
departments | ["Sales", "Marketing"] | Fixed values: Engineering, Product, Design, Sales, Marketing, Customer Success, Operations, Finance, People & HR, Legal, IT, Data, Research, Support, Business Development, Consulting |
job_functions | ["Human Resources"] | Fixed values: Engineering, Sales, Marketing, Operations, Finance, Human Resources, Information Technology, Product Management, Consulting, Business Development |
locations | ["Toronto, Ontario, Canada"] | Full literal location string |
location_city | ["Toronto"] | Bare city name |
location_state | ["Ontario"] | |
location_country | ["Canada"] | |
school_names | ["Stanford University"] | Schools attended |
skills | ["Salesforce"] | Skills listed on the profile |
languages | ["French"] | Languages listed on the profile |
company_names | ["Shopify"] | Current employer |
company_domains | ["shopify.com"] | Current employer; more reliable than names |
company_industries | ["Software Development"] | LinkedIn industry names |
company_headcount | ["51-200", "1,001-5,000"] | Fixed bands: 1-10, 11-50, 51-200, 201-500, 501-1,000, 1,001-5,000, 5,001-10,000, 10,001+ (the forms without commas, such as 501-1000, also work) |
company_locations | ["San Francisco"] | Current employer’s HQ location |
Get valid values for titles, seniority, departments, job_functions, management_levels, locations, company_headcount and company_industries from GET /people/find/filters?field=<field>&query=<text> (free).
Unknown keys and values are rejected, not ignored. A key outside this table returns 400 validation_error, and so does a value outside a fixed list (such as management_levels: ["Individual Contributor"] or company_headcount: ["50-200"]). The error names the problem and suggests the fix: company-only keys like funding_stages, revenue_ranges, technologies and year_founded point you to company search, and near-misses like industries point you to company_industries.
{
"error": {
"code": "validation_error",
"problem": "Unsupported search_people filter key(s): ['funding_stages']. These were previously ignored silently; they are now rejected so a dropped constraint never returns the wrong people.",
"fix": "Remove the unsupported key(s). Suggested: funding_stages → use search_companies (funding is a company-only filter). Valid keys: ...",
"retryable": false,
"docs_url": "https://docs.cleanlist.ai/errors/validation-error",
"request_id": "req_01J...",
"details": {
"unsupported_keys": ["funding_stages"],
"suggested_replacements": { "funding_stages": "use search_companies (funding is a company-only filter)" },
"allowed_keys": ["company_domains", "company_headcount", "..."]
}
}
}Why does filtering by titles return almost nothing?
Because titles is an exact string match. Searching titles: ["VP of Sales"] misses “VP Sales”, “Vice President, Sales”, “VP Global Sales” and every other real-world variant, so a title search that should return thousands returns a handful.
Use management_levels plus departments instead. {"management_levels": ["VP"], "departments": ["Sales"]} captures every phrasing of the same role. Reserve titles for when you already know the exact string, such as re-finding a specific person. If the criteria form is enabled on your workspace, its jobTitleMatchType: "contains" matches titles by substring with synonyms, the way the Find People page does.
How many results can one search return?
limit accepts 1 to 500 and defaults to 25, but a plain search returns at most 25 rows per call. Ask for more and you get 25 plus a cursor for the next page. “Plain” means a flat filters search, or a criteria search whose route.mode is plain. The total field reports the full match count regardless of limit, so you can size a segment before paging through it.
A criteria search that routes company-first (route.mode: "by_companies", usually because you sent company_criteria) isn’t held to 25 rows: it returns up to limit, or criteria.maxResults when that is smaller.
For example, a search for VPs of Sales in Canada at 51-200 employee companies returned total: 2705 with limit: 3 when we tested it in August 2026. Sizing a segment costs no credits.
How do I paginate through People Finder results?
Every response carries a cursor. Pass it back as cursor on the next request, with the same filters, to get the following page. When cursor comes back null, you have reached the end.
curl -X POST https://api.cleanlist.ai/api/v2/people/find \
-H "Authorization: Bearer clapi_your_key" \
-H "Content-Type: application/json" \
-d '{"filters": {...}, "limit": 25, "cursor": "H4sIAJLvg2oC_xXMMQ..."}'Cursors are opaque; don’t construct or modify one. There is no offset or page-number pagination, which keeps results stable while you page. Each page is a separate search for the daily cap below.
Cursor pages of a plain search are also rationed per hour: an organization can load 40 cursor pages an hour (about 1,000 rows at 25 a page). The first page of a new search never counts. Past the cap you get 429 with “Hourly preview paging limit reached.” and a Retry-After header set to the end of the hour. To pull more than that, save the cohort to a list with its task_id instead of paging.
What does a People Finder response look like?
{
"task_id": "cl-task_Wr26-XlP",
"timestamp_ms": 1787031442496,
"entity_kind": "person",
"results": [
{
"lead_id": "28685",
"first_name": "Jane",
"last_name": "Doe",
"full_name": "Jane Doe",
"title": "Senior Vice President Business Development",
"company": "Example Bio",
"domain": "example.com",
"location": "Montreal, Quebec, Canada",
"linkedin_url": "https://www.linkedin.com/in/jane-doe-example",
"seniority": "Vice President",
"confidence_score": null
}
],
"total": 2705,
"cursor": "H4sIAJLvg2oC_xXMMQ...",
"credits_charged": 0,
"truncation_warning": null
}Search results carry identity and firmographics. They do not carry an email address or a phone number; those come from enrichment, which is where credits are spent. Rows can also include company fields such as company_linkedin_url, company_industry and company_headcount_range.
confidence_score is always null on search results today. Treat any field you have never seen populated as unavailable rather than as a signal.
What is the task_id, and why does it matter?
task_id identifies the cohort your search just produced. It connects search to enrichment and to saving.
- Enrich the cohort with
POST /api/v2/enrichment/by-task(thetask_id, theentity_idsyou want, and a quote). See Enrichment. - Save it to a list with
POST /api/v2/lead-lists/{list_id}/leadsand{"task_id": "..."}. Saving is free. See Lead Lists. - Read it back with
GET /api/v2/tasks/{task_id}, without running the search again. See below.
Handles expire an hour after they are created.
GET /tasks/{task_id}
Scope: depends on the cohort: people:read for a people search, companies:read for a company search, lists:read for a list read · Cost: free
Pages through the cohort behind a task_id. It works for handles from POST /people/find, POST /search/companies, GET /lead-lists/{id}/leads and enrichment dispatches. Search rows come back as the snapshot the search returned; saved leads are re-read live from the list, so they show current emails and phones.
| Query param | Default | Notes |
|---|---|---|
offset | 0 | Position in the cohort (or in your entity_ids selection) |
limit | 25 | 1 to 100 |
entity_ids | none | Repeat to read only these ids, in this order (up to 2,000). An id outside the cohort is a 400 |
snapshot_only | false | true returns the cached rows only, without re-reading saved leads |
columns | none | Search cohorts only, up to 12 extra preview columns. People: Seniority, Function, Department, Sub-department, Time in role, Time at company, Total tenure, Role start, LinkedIn, Domain, Company LinkedIn, Founded. Companies: Revenue, Funding, Last funding, Founded, LinkedIn |
curl "https://api.cleanlist.ai/api/v2/tasks/cl-task_Wr26-XlP?limit=25&offset=25" \
-H "Authorization: Bearer clapi_your_key"The response carries tool (which call made the cohort), entity_type (person, company, lead or list), total_entities, selected_count, next_offset (null at the end), expires_at, workflow_id (for an enrichment cohort) and entities. Each entity row has entity_id, details, data_source (snapshot, live or unavailable) and, for enrichment cohorts, enrichment_status. Search cohorts also return a presentation block of display-ready rows.
An unknown, expired or other-workspace task_id returns 404 task_not_found; the three look the same on purpose. Run a new search for a new handle, and review it before acting: it may not be the same people. This endpoint isn’t available to AppSumo-only workspaces (403 feature_gated).
lead_id on a search result is a search-scoped identifier. It does not work as a lead_ids value on /lead-lists/{id}/leads. Use task_id (plus optional entity_ids) to act on a search cohort.
The structured criteria form
Instead of filters, you can send criteria: the same filter form the Find People page uses, compiled on the server the same way. Send exactly one of filters or criteria. With criteria you also get:
- Title matching by substring and synonyms (
jobTitlesIncludewithjobTitleMatchType: "contains"). - Person and employer geography, past roles and employers, education, skills, years in role and more (73 fields).
company_criteriafor employer attributes such as revenue, funding or growth, which may route the search through a company-first path.- Exclusions: people already in your CluGTM lists (
excludeInCleanlist) or in your connected CRM (withcrmnaming the CRM). sort, andcriteria.maxResultsas a per-response ceiling (1 to 500).
{
"criteria": {
"jobTitlesInclude": ["VP Sales"],
"jobTitleMatchType": "contains",
"contactCountries": ["Canada"],
"companySizes": ["51-200"]
},
"limit": 25
}The response adds route (mode is plain or by_companies, with the reason and the fields that triggered_by it), warnings, exclusions_applied and a search_handoff.url that opens the same search on the Find People page. Rows may carry match_evidence with status: "unverified" when the current role or company couldn’t be confirmed. The full field list is in the PeopleSearchCriteria and CompanySearchCriteria schemas of the OpenAPI spec.
The criteria form isn’t available to AppSumo-only workspaces, which get a 403 instead. Errors specific to this form: invalid_criteria, empty_criteria, unsupported_exclusions and crm_not_connected (all 400).
Look up accepted values
An invalid_criteria error tells you to check the field against GET /api/v2/search/values or GET /api/v2/reference?section=filters. Both are free, make no provider calls on fixed lists, and accept a key with people:read or companies:read. Like the criteria form, they return 403 to AppSumo-only workspaces.
GET /search/values
Returns the values the compiler accepts for one criteria field.
| Query param | Notes |
|---|---|
field | Required. A criteria field name, camelCase or snake_case (seniority, companySizes, contactCountries, industriesInclude) |
entity | people or company. Needed only for names both forms share, such as industriesInclude |
query | Substring filter for fixed lists; required for free-text fields, where it is the autocomplete text |
limit | 1 to 500, default 50 |
curl "https://api.cleanlist.ai/api/v2/search/values?field=seniority" \
-H "Authorization: Bearer clapi_your_key"{
"field": "seniority",
"entity": "people",
"match": "enum",
"exhaustive": true,
"options": [{ "value": "CXO", "label": null, "note": null }],
"total": 8,
"timestamp_ms": 1787031442496
}match says how the value is compared once compiled: substring, exact, enum, range, date, boolean or structured. exhaustive: true means the list is the complete accepted set, so anything else is rejected. For a free-text field such as a title, options are autocomplete suggestions, not a closed set, and leaving out query returns 400 query_required. An unknown field returns 400 unknown_field with examples. Send each value verbatim.
GET /reference
Returns the long-form reference that the CluGTM MCP reads, generated from the code that enforces it. Omit section to list the sections; pass one to read it as Markdown in content. Sections: filters, filters-people, filters-company, costs, quotes, limits, crm, prospecting-workflow, crm-insights, draft-messaging and review-messaging. An unknown section returns 404 unknown_section.
curl "https://api.cleanlist.ai/api/v2/reference?section=filters-people" \
-H "Authorization: Bearer clapi_your_key"Neither endpoint is in the served OpenAPI spec yet; call them over HTTP as shown.
Compile a search without running it
POST /people/find/compile
Scope: people:read · Cost: free, doesn’t count as a search
Shows what a criteria search would do without running it. Body: criteria (required), optional company_criteria and crm. The response returns route, the compiled person_filter and company_filter, warnings, exclusions_applied and exclusions_complete. Use it to check an unfamiliar combination of criteria, or to explain why a result is bounded. It has the same availability as the criteria form.
Find people at companies by filter
POST /people/find-at-companies
Scope: people:read · Cost: free
Starts an async search for people at a set of companies. Give a company filter and/or explicit company LinkedIn URLs, plus a person filter; the two are combined with AND.
| Field | Type | Notes |
|---|---|---|
company_filter | object | Company filter in the raw company search shape, for example {"op": "and", "conditions": [{"column": "linkedin_industries", "type": "in", "value": ["Software Development"]}]} |
company_linkedin_urls | string[] | Explicit company LinkedIn URLs. Intersected with company_filter when both are sent |
person_filter | object | Required. People filter in the raw group shape, for example {"type": "group_filter", "op": "and", "filters": [{"column": "title", "filter_type": "anyof", "value": ["CEO"]}]} |
limit | integer | 1 to 1000, default 100 |
cursor | string | From a previous status response, to get the next block |
You must send company_filter, company_linkedin_urls or both. The column names are the ones the flat keys map to (for example title, seniority_level and region for people; linkedin_industries, employee_count_range and hq_location for companies). If you just need people at a few known domains, POST /people/find with company_domains is simpler.
The response returns task_id, status: "processing" and a poll_url.
GET /people/find-at-companies/status/{task_id}
Scope: people:read · Cost: free
Poll until status is completed or failed. The completed response carries results (same row shape as /people/find), total, cursor, credits_charged: 0, resolved_company_count (how many companies the filter resolved to) and an optional composite_warning. One job returns one block of results; for the next block, call POST /people/find-at-companies again with the returned cursor.
The search runs inside the API server. If the server restarts while it runs, the poll answers failed once the job has gone 60 seconds without checking in; start the search again.
Find people at companies already in a list
POST /lead-lists/{list_id}/find-people
Scope: lists:write
Starts an async job that finds people at companies already in the list and imports them into that list.
| Field | Type | Notes |
|---|---|---|
company_ids | string[] | Required. 1 to 100 CluGTM company ids of companies in this list (added directly, or employers of leads in the list). Ids outside the list are dropped; if none remain you get 400 |
job_titles | string[] | Optional title filter |
seniority_levels | string[] | Optional seniority filter |
limit_per_company | integer | 1 to 25, default 5 |
location_state | string | Optional |
location_country | string | Optional |
The response returns workflow_id, status: "pending", total_companies, estimated_credits, lead_list_id and a poll_url.
Cost: discovery and the import are free, so credits_used reports 0. Before the job starts, CluGTM checks that your balance covers estimated_credits (companies × limit_per_company) and that the list has room under its contact limit; otherwise you get 402 insufficient_credits or a 403.
GET /lead-lists/{list_id}/find-people/status/{workflow_id}
Scope: people:read · Cost: free
Poll until status is completed, failed or cancelled. The response carries people_found, people_imported, credits_used, the new lead_ids and any errors.
find-at-companies and find-people aren’t available to AppSumo-only workspaces (a paid CluGTM plan on top lifts this). They return 403 with feature_gated.
What does it cost to turn search results into contact data?
Search is free. Enrichment is billed per result found:
| What you get | Credits |
|---|---|
| Verified work email | 1 |
| Direct-dial phone | 10 |
| Email and phone | 11 |
| Company enrichment | 1 |
| Save a lead to a list | 0 |
| CRM or sequencer sync | 0.2 per lead |
| Search | 0 |
Enrichment is pay-for-results. If the waterfall returns nothing for a record, you are not charged for that record.
Search is free on every standard plan. A workspace whose contract sets a per-row search price is charged that price for the rows each call asks for (credits_charged shows it, a failed search is refunded, and a balance that can’t cover it returns 402 insufficient_credits).
How do I stop a job from overspending?
Paid bulk operations require a signed quote. Call POST /api/v2/credits/estimate first. It returns a quote_id (HMAC-signed, 5-minute TTL, single use), the estimated cost, your available credits and a sufficient boolean. Pass the quote_id to the enrichment call or the server rejects it.
This is what makes CluGTM safe to hand to an autonomous agent: an agent can’t run a paid bulk job it hasn’t priced first, and the server checks the quote against the actual cohort with a 10% row-count tolerance and a hard credit cap.
What are the People Finder API rate limits?
| Limit | Value |
|---|---|
| Requests per minute, per organization | 60 (30 for AppSumo licenses without a paid plan) |
| Requests per minute, per API key | 30 |
| Searches per UTC day, per organization, from API keys | 60, shared by all keys |
| Cursor pages per hour, per organization (plain searches) | 40 |
| Searches per minute, per organization, from the CluGTM MCP | 60 |
The daily cap counts every POST /people/find call made with your organization’s API keys, including cursor pages, and resets at midnight UTC. When it’s hit you get 429 with the message “People Search rate limit exceeded. Public API keys are limited to 60 searches per day per organization. Use the CluGTM MCP for higher throughput.” and a Retry-After header. At 25 rows per plain search, that is up to 1,500 rows a day. A company-first criteria search returns up to limit rows per call, so it isn’t bounded the same way. For higher volume, save cohorts to a list with task_id or use the CluGTM MCP. find-at-companies and company search don’t count toward this cap.
The hourly cap counts only cursor pages of a plain search; the first page of each search is free of it. It answers 429 with “Hourly preview paging limit reached.” and a Retry-After to the top of the hour.
How does search count against my plan?
On Pro and Enterprise, searches are unlimited (within the rate limits above). On AppSumo, each search draws from the workspace’s monthly search allowance (200 a month, shared with company search); when it runs out, search returns 403 search_quota_exhausted until the allowance resets.
Does the People Finder API accept natural-language queries?
No. POST /people/find takes structured filters only. There is no natural-language parameter.
Natural language works one level up. The CluGTM MCP in Claude and Clu in the app translate a sentence like “VPs of finance in Ontario at Shopify” into {"management_levels": ["VP"], "departments": ["Finance"], "location_state": ["Ontario"], "company_names": ["Shopify"]} and run the search for you.
Can I filter people by revenue, funding stage, or tech stack?
Not on /people/find with flat filters: funding_stages, revenue_ranges, technologies and similar keys are rejected with 400 validation_error and a pointer to company search. There is no technographic filter and no intent data on the API.
Two ways to combine company attributes with people:
- Run a company search first, then pass the domains to
/people/findascompany_domains. - If the
criteriaform is enabled on your workspace, sendcompany_criteria(for examplerevenueMin,fundingRoundTypes,growthMin) with your peoplecriteria.
What error does a bad request return?
Errors from the endpoint use the public envelope, wrapped in an error object:
{
"error": {
"code": "insufficient_scope",
"problem": "This call requires the 'people:read' scope, which is not granted to the authenticating credential.",
"fix": "Either re-issue the API key with the required scope, or sign in with an identity that has it. The full list of granted scopes is visible via /api/v2/whoami.",
"retryable": false,
"docs_url": "https://docs.cleanlist.ai/errors/insufficient-scope",
"request_id": "req_01J...",
"details": { "required_scope": "people:read", "granted_scopes": ["lists:read"] }
}
}code is stable and safe to branch on. retryable tells you whether a retry can succeed. Rate limits (429) and authentication failures (401) use the shorter generic shape. Full table in Errors & Rate Limits.
How do I go from a search to a CRM in one pass?
POST /people/findwith your filters. Free. Note thetask_id.POST /lead-lists/{list_id}/leadswith{"task_id": "..."}to save the people you want. Free.POST /credits/estimatewithtool: "enrich_list"and thelist_id, thenPOST /enrichment/bulkwith thequote_id. This is where you spend.- Poll
GET /enrichment/status/{workflow_id}until it completes. There are no v2 webhooks, so polling is the contract. See Polling & Async. - Quote
sync_to_crm, thenPOST /migrate/crmwithproviderhubspotorsalesforce, thequote_idandapproved: true(0.2 credits per lead). For a Lemlist campaign, usePOST /migrate/sequencer. See CRM & Sequencer Sync.
Steps 1 and 2 cost nothing, so you can build and size the whole segment before committing any spend.
Where do the results come from?
CluGTM is an orchestration layer, not a data vendor. People Finder is served by a search provider, and contact enrichment runs a multi-provider waterfall that queries sources in sequence until a verified result comes back. Email verification runs through dedicated verification providers before a result is returned to you.
That architecture is why enrichment is pay-for-results: CluGTM only bills when a provider returns something that passes verification. On the CluGTM 500-Lead Enrichment Benchmark, our own test run on a 500-lead sample, that produced 98% verified work email and 85% direct dial.
FAQ
Is the CluGTM People Finder API free?
The search call itself is free and returns credits_charged: 0 on every response. API keys need a Pro or Enterprise plan (or legacy Pro or Scale) or an AppSumo Tier 5 to 9 license. Enrichment of search results is billed separately at 1 credit per verified email and 10 per direct-dial phone.
What is the difference between seniority and management_levels?
Both filter on the person’s seniority level. seniority takes the full list of levels (Owner, Founder, C-Suite, Partner, Vice President, Head, Director, Manager, Senior, Entry, Trainee, Unpaid). management_levels is a short list of management tiers (C-Suite, VP, Director, Manager) where VP matches “Vice President”. For most role-based searches, management_levels plus departments is the reliable pair.
Can I search for people at a specific company?
Yes. Use company_names: ["Shopify"] or, more reliably, company_domains: ["shopify.com"]. Domains avoid the ambiguity of companies that share a name.
How do I get an API key?
In the app, go to Settings → API keys, fill in Create API key and click Create API key. Keys start with clapi_ and are shown once. You can hold up to 10 active keys per user per organization. See Authentication.
Does People Finder work over MCP?
Yes. The CluGTM MCP exposes search_people with the same filters and the same zero credit cost, and it isn’t held to the 60-searches-a-day cap for API keys (it is limited to 60 searches per minute per organization). The MCP works on Starter, Pro, Enterprise, the Pro trial and every AppSumo tier.
Last updated: November 10, 2026. Filter keys, limits, costs and error shapes on this page were checked against the API code and the live OpenAPI spec on that date.