New: meet Clu, and send email, LinkedIn and call steps with Sequences. Watch the videos →
API Reference (v2)
People Finder API

People Finder API

Cleanlist'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 Cleanlist MCP.

How do I call the Cleanlist 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 -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:

KeyExampleNotes
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 one response returns at most 100 rows. Ask for more and you get 100 plus a cursor for the next page. The total field reports the full match count regardless of limit, so you can size a segment before paging through it.

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": 100, "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.

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 (the task_id, the entity_ids you want, and a quote). See Enrichment.
  • Save it to a list with POST /api/v2/lead-lists/{list_id}/leads and {"task_id": "..."}. Saving is free. See Lead Lists.

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 (jobTitlesInclude with jobTitleMatchType: "contains").
  • Person and employer geography, past roles and employers, education, skills, years in role and more (73 fields).
  • company_criteria for employer attributes such as revenue, funding or growth, which may route the search through a company-first path.
  • Exclusions: people already in your Cleanlist lists (excludeInCleanlist) or in your connected CRM (with crm naming the CRM).
  • sort, and criteria.maxResults as a per-response ceiling (1 to 500).
{
  "criteria": {
    "jobTitlesInclude": ["VP Sales"],
    "jobTitleMatchType": "contains",
    "contactCountries": ["Canada"],
    "companySizes": ["51-200"]
  },
  "limit": 25
}

The response adds route (which path ran and why), 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).

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.

FieldTypeNotes
company_filterobjectCompany filter in the raw company search shape, for example {"op": "and", "conditions": [{"column": "linkedin_industries", "type": "in", "value": ["Software Development"]}]}
company_linkedin_urlsstring[]Explicit company LinkedIn URLs. Intersected with company_filter when both are sent
person_filterobjectRequired. People filter in the raw group shape, for example {"type": "group_filter", "op": "and", "filters": [{"column": "title", "filter_type": "anyof", "value": ["CEO"]}]}
limitinteger1 to 1000, default 100
cursorstringFrom 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.

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.

FieldTypeNotes
company_idsstring[]Required. 1 to 100 Cleanlist 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_titlesstring[]Optional title filter
seniority_levelsstring[]Optional seniority filter
limit_per_companyinteger1 to 25, default 5
location_statestringOptional
location_countrystringOptional

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, Cleanlist 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 Cleanlist 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 getCredits
Verified work email1
Direct-dial phone10
Email and phone11
Company enrichment1
Save a lead to a list0
CRM or sequencer sync0.2 per lead
Search0

Enrichment is pay-for-results. If the waterfall returns nothing for a record, you are not charged for that record.

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 Cleanlist 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?

LimitValue
Requests per minute, per organization60 (30 for AppSumo licenses without a paid plan)
Requests per minute, per API key30
Searches per UTC day, per organization, from API keys60, shared by all keys
Searches per minute, per organization, from the Cleanlist MCP60

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 RATE_LIMITED with the message "Public API keys are limited to 60 searches per day per organization. Use the Cleanlist MCP for higher throughput." and a Retry-After header. At 100 rows per response, that is up to 6,000 rows a day. For higher volume, use the Cleanlist MCP. find-at-companies and company search don't count toward this cap.

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 (50 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 Cleanlist 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/find as company_domains.
  • If the criteria form is enabled on your workspace, send company_criteria (for example revenueMin, fundingRoundTypes, growthMin) with your people criteria.

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?

  1. POST /people/find with your filters. Free. Note the task_id.
  2. POST /lead-lists/{list_id}/leads with {"task_id": "..."} to save the people you want. Free.
  3. POST /credits/estimate with tool: "enrich_list" and the list_id, then POST /enrichment/bulk with the quote_id. This is where you spend.
  4. Poll GET /enrichment/status/{workflow_id} until it completes. There are no v2 webhooks, so polling is the contract. See Polling & Async.
  5. Quote sync_to_crm, then POST /migrate/crm with provider hubspot or salesforce, the quote_id and approved: true (0.2 credits per lead). For a Lemlist campaign, use POST /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?

Cleanlist 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: Cleanlist only bills when a provider returns something that passes verification. On the Cleanlist 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 Cleanlist 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 7 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 Cleanlist 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: October 7, 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.