You need an API key with the
search scope for every endpoint on this page, plus the lists scope for the final polling step — see authentication and scopes. Searching and saving both debit search credits; see credits.1
Look up the accepted filter values
Enumerated filters (industries, seniority levels, locations, company sizes, and so on) only accept values from a fixed vocabulary. A value outside it is refused with Copy the
422 VALIDATION_FAILED, naming the field and the value, before anything is billed. Matching ignores case. So before you write filters by hand, fetch the vocabulary:value of each entry, not its label. Free-text filters are not checked this way, so if a search that should obviously match returns nothing, look at those next.jobTitleRoleToSubRole is still returned but is deprecated: sub-department is no longer a search filter (see Removed filters).Free-text fields (locations, titles, skills, schools, company names, websites) have no fixed list to enumerate — for those, GET /business/prospects/autocomplete suggests fully-qualified values:field is one of location, region, title, skill, school, company, website, and every field answers from the first character. A partial string like "San Fran" matches nothing in a search; the suggestion’s fully-qualified form does. Titles, regions and locations come in lower case, a location as city, state, country. Company, school and skill names keep their own spelling, and a website suggestion is the company’s bare domain.region suggestions come from the table of state and region names described below, so every suggested region can be searched. Each one carries a countries array, because the same region name can exist in several countries. Pair the region with a location_country filter to disambiguate, like the app does.Autocomplete’s suggestions come from a different data source than the search itself, so a suggestion is not guaranteed to match: a suggested company name can still be refused with 422 FILTER_VALUE_UNRESOLVED. To pin a company, identify it by liOrgId or domain as described in Company names.location_region and job_company_location_region values are matched against a table of known state and region names per country. A name outside that table is still searched, as written, when exactly one country is selected in the matching country filter (location_country or job_company_location_country). When none of the field’s include values can be matched, the search is refused with 422 FILTER_VALUE_UNRESOLVED rather than run without them, even if an exclude value matches; a field of exclude values only is refused when none of them matches. A value that cannot be matched next to ones that can is dropped. Selecting the country as well usually fixes it.2
Preview the search
POST /business/prospects/search runs the search and returns one page of matching profiles. Enumerated filters take an array of { "status": "include" | "exclude", "value": "..." } objects — exclude flips a value into an exclusion. A search needs at least one include filter, such as an include entry or a job title: one made only of exclude entries is refused with 422 SEARCH_FILTERS_INVALID, and nothing is billed. size controls the page size (1–100, default 10).savedProfile/savedBy, which tell you whether someone on your team already saved that person. Names, job titles and company names come as written on the profile; industry and location_name are lower case. Emails and phones are never present here. They arrive when you save to a list and enrichment runs.total is an estimate of how many people match the whole query (not the page size). Only the first page carries it; later pages answer null. The first page answers null too on a search with employer filters (company size, revenue, industry, headquarters location, funding, or anything in company_filters), because such a search does not count its matches. Decide whether a search found anyone from profiles, never from total. filters echoes the filter set that actually ran. With literal filters the echo is what you sent; with the alternative sources below it is what your input resolved to: inspect it, refine it, and send it back as filters on the next call or on save-to-list.Three ways to express the search
Three ways to express the search
The body carries exactly one filter source; combining two returns a A
422.Literal filters — the structured form shown above.A saved search — savedSearchId, or savedSearchName for an exact title match. Names resolve strictly: no match is a 404, and two searches sharing the title is a 409 SAVED_SEARCH_NAME_AMBIGUOUS — the API never silently picks one. The response additionally carries ignoredFilterKeys (see Saved searches).A natural-language query — the same smart search as the app’s prompt box. The prose is converted into structured filters server-side, the search runs on those, and the response echoes them plus unresolvedCriteria: the parts of your prose that could not be mapped to any filter, quoted verbatim. A non-empty array means the results are broader than your sentence — tighten by editing the echoed filters directly.query too vague to yield any filter returns 422 QUERY_FILTERS_INVALID rather than running (and billing) an unconstrained search. Query conversion also has its own tighter rate limit on top of the search one, since it runs a model per call.linkedin_url: match and de-duplicate rows on it. You get a contact id when a person enters your workspace, as the id on list rows after a save, or from POST /business/contacts/resolve with the LinkedIn URL.To fetch the next page, repeat the request with the same filters and the scrollToken from the previous response in the body. The last page, usually shorter than size, comes back with scrollToken: null, and so does a profile lookup whose people all fit in size. A page can also come back a little short while more pages follow, because repeated people are removed, so keep paging until scrollToken is null. A token this API did not issue, including one issued before the people search changes in the changelog, is refused with 422 SCROLL_TOKEN_INVALID: start the search again without it. See pagination for how scroll tokens behave.Each page you fetch debits search credits, so keep size small while you tune filters. Previewing is the cheap way to tune filters before you commit to saving.A search has about 28 seconds. When its own lookups of company names, email addresses or phone numbers run out of time, it answers 503 whose code is UNEXPECTED_ERROR; the lookups that finished are kept, so the same request sent again gets further. When the search provider takes too long, it answers 504 with the same code. Nothing is billed either way. If it times out again, narrow it: searches on the employer’s attributes are the slowest, so add a job title or a location, or use fewer employer filters.Filter shapes and the full key list
Filter shapes and the full key list
The
filters object accepts these shapes of field, plus a rangeInputs object for numeric ranges:Several keys have their own matching rules: names match loosely,
job_title also matches common forms of a title, keywords match whole words, linkedin_url takes a URL or a bare slug, work_email, personal_emails and mobile_phone look the person up by that email address or phone number, job_start_date is a window rather than an exact month, and each company size or revenue bracket is searched on its own. See Filter rules for these, and Removed filters for the keys this table no longer lists.A search has to narrow the results. One whose filters narrow nothing once they are translated, for example a filter set made only of exclude entries, is refused with 422 SEARCH_FILTERS_INVALID instead of running.rangeInputs sits alongside the other filters and takes { "min": number, "max": number } objects for job_company_employee_count, inferred_years_experience, and job_company_total_funding_raised. Either bound can be left out, and a bound of 0 or less means no bound: { "min": 50, "max": 0 } finds 50 or more. A min above a max greater than 0 is refused with 422 VALIDATION_FAILED, and nothing is billed. A saved search that stores one answers 422 SAVED_SEARCH_INVALID.3
Save the results into a list
When the preview looks right, save the matches with The response is a
POST /business/prospects/search/save-to-list. The body takes the same filter source as the search — filters, savedSearchId/savedSearchName, or query (exactly one) — plus a limit for how many rows to save (1–10,000, required) and a destination: listId for an existing list, or listName, which creates the list or reuses the existing one with that name, ignoring case (a name shared by two lists is a 409 LIST_NAME_AMBIGUOUS, so use listId instead; a name reserved for the app’s own lists is a 422 LIST_NAME_RESERVED).enrich selects what runs on the saved rows:With
enrich: "none", saving costs 1 credit per row saved, and your balance must cover 1 credit per row of limit before the save starts.With an enrichment mode, one enrichment job runs the search, saves the rows and looks up their contact data. Saving the rows adds no charge of its own: the job bills for the values it finds, at the enrichment prices, and contacts that already carry the requested data are not re-charged. They still count toward limit, so saving the same search again takes the same people. Before the job starts, your balance must cover limit × the enrichment’s highest per-row price: 50 credits for email (100 when personal emails are prioritised), 200 for phone, and the two added together for phone_and_email.When the balance falls short, the call is refused with 402 INSUFFICIENT_CREDITS and nothing is billed.An optional campaignId or campaignName (strict resolve — 404 on a miss, 409 CAMPAIGN_NAME_AMBIGUOUS on a shared name) attaches the destination list to that campaign in the same call.202: the job has started, not finished. The search runs server-side, and contacts appear in the list as saving (and enrichment, when requested) completes. Enriched saves also report totalContacts (the limit you sent), estimatedMinutes, and the enrichment run’s jobId and status (pending until the job starts, which waits for any other enrichment job of yours to finish, then processing); a campaign attach reports back as "campaign": { "id": "…", "attached": true }.Note that preview and save use different sizing fields on purpose: size is a page size for browsing results, while limit is the total number of rows the save job will pull and enrich.Campaign-attach timing: the attach happens when the save starts. A draft campaign enrolls the full list when you approve it, but an ACTIVE non-dynamic campaign enrolls only the rows present at attach time — approve after the list fills, or use a dynamic campaign, to reach everything saved.
4
Poll the list until it fills
With This endpoint uses page-number pagination (
enrich: "none", poll the list itself until its completionStatus is complete. Row counts climb while the job runs, so a list that already has rows is not necessarily finished.With enrich set, follow the run at GET /business/jobs/{jobId} until its status is completed, stopped, failed or cancelled. Do not wait on the list here: it reads complete until the job starts, so it can look finished before any rows land. The job reports its counts and credits so far, and a stopped job paused because the balance ran out. See long-running work.metrics counts the rows that came back with a work email, a personal email or a valid phone. Once the list is complete, read its rows:pageNum and limit, max 1000 per page) rather than scroll tokens — see pagination. A wrong or inaccessible listId returns a 404 with the standard error envelope:Row fields
Every profile carries the same fields. A person field with no value is left out of the row rather than sent asnull or an empty string.
Rows no longer carry
_id, pdlId, sex, job_title_class, profile_score, activity_score, dataset_version, pdl_version, job_change_detected_at, createdAt, updatedAt or any other field not in this table.
Filter rules
Company names
job_company_name and experience.company.name (past employers) match a company, not a piece of text. A name still works on its own: "brightpay" matches every company named exactly Brightpay, ignoring case. To pick one company, add its LinkedIn organization id as liOrgId, its website as domain, or both, to an include/exclude entry. value stays the company name:
linkedin_id is the liOrgId, and any of its domains works as domain. experience.company.name takes the same values, include only.
An entry that matches no company is dropped when other entries in the field do match. When none of the field’s include entries matches a company, the search is refused with 422 FILTER_VALUE_UNRESOLVED, even if an exclude entry matches, and so is a field of exclude entries that all name companies nobody has. param names the field, for example filters.job_company_name. An exclude entry that could not be looked up (past the lookups a search makes, out of time, or a failed lookup) refuses the search with 422 COMPANY_EXCLUDES_UNRESOLVED instead, because running without it would return the people it excludes. Save-to-list does not check names before it answers: with enrich: "none", a save whose company names all fail to match is accepted with a 202, and the list build then fails. Run the same filters through a search first.
A search looks up at most 500 company names per field that Fuse’s own company records and its earlier lookups do not answer. Entries that carry liOrgId or domain need no lookup and do not count. Included names past that, or whose lookup did not finish in time, are left out of that search without stopping it, and the response says so in unappliedCompanyNames: one entry per field with totalCount (include entries sent), unappliedCount (left out, names nobody has included) and pendingLookupCount (not looked up yet). Sending the same request again looks the next ones up and applies more. Save-to-list with enrich: "none" runs its own search, so the same holds for it; run the same filters through a search first to see unappliedCompanyNames. With enrich set, the save’s job looks up all the names first, before it saves anyone, and fails without saving or charging anything if its lookups still have not all finished after about 25 minutes (see long-running work). To have every company apply from the first search, identify them by liOrgId or domain.
job_company_website and experience.company.website take website domains such as brightpay.com; a scheme, www. or a path is ignored. A value that is not a domain, such as brightpay, is dropped. When none of a website filter’s include values is a domain, the search matches nobody: it answers an empty page with total: 0, and nothing is billed.
LinkedIn profiles
The top-levellinkedinUrl looks up one person, and the linkedin_url filter matches people by profile. Both take a profile URL in any form (with or without https://www.), in/<slug>, or the bare slug, such as ada-nwosu, and older slugs of the same person match too. When the profiles looked up all fit in size, the lookup is one page: it answers with scrollToken: null, and total is the number of rows found. More profiles than that page on like a search, with total null.
When no include value can be resolved to a LinkedIn profile, the search is refused with 422 FILTER_VALUE_UNRESOLVED. param is filters.linkedin_url for the filter, or linkedinUrl for the top-level lookup.
Email and phone filters
work_email, personal_emails and mobile_phone find the person who has that email address or phone number, then search that person with every other filter you send. Work and personal addresses are looked up the same way, so either key finds a person by any of their addresses. Send a phone number in E.164 with the leading +, such as +14155552671; a number without a country code is read as a US number.
Each key takes a string or an array of up to 10 values, and the values in one key are OR-joined. Send several of these keys and the person must have all of them. Each value finds at most one person, its best match, and a company’s number, such as a front desk, finds no one. When no value finds anyone, the search returns no rows and bills nothing. Like a LinkedIn lookup, when the people found all fit in size the search is one page, with scrollToken: null and total the number of rows found.
When no value of one of these keys is an email address or a phone number, the search is refused with 422 FILTER_VALUE_UNRESOLVED, and param names the key, for example filters.work_email. The error’s message suggests autocomplete, which has no email or phone field: correct the values themselves.
Name filters
full_name, first_name and last_name are matched loosely rather than as an exact term: case does not matter, and close variants of a name can match. first_name and last_name sent together are matched as one full name. Check the names on the returned rows rather than assuming each one is exactly the name you sent.
Job titles and keywords
job_title matches the exact title in any case, together with its common forms: vp of sales also finds vice president of sales and vp sales. With job_title_levels, every requested level still applies. The owner, partner, entry, training and unpaid levels are read from the title’s words, so founder with owner keeps only the title forms that name an owner; a title that has no form for any requested level returns 422 SEARCH_FILTERS_INVALID rather than a wider search. keywords and excludedKeywords match whole words in the current job title, in any case, and add plural and related forms: growth matches Head of Growth but not Growthwise, and engineer also finds engineers and engineering. * and ? are ignored.
Company size and revenue
job_company_size and job_company_inferred_revenue take include and exclude entries. Each bracket is searched as its own band: several include brackets match any of them, so 11-50 plus 501-1000 does not find companies of 51 to 500 people, and every exclude removes its bracket. For a headcount range that does not follow the brackets, use rangeInputs.job_company_employee_count.
Employer filters
company_filters narrows the search to people whose current employer matches company search’s own filters. It takes an object with the same keys and value shapes as filters on POST /business/companies/search, with values from GET /business/companies/filter-options, limited to these keys: status, tags, stage, fortuneRankings, industriesV2, linkedinIndustries, crunchbaseCategories, crunchbaseCategoryGroups, naicsCodes, officeLocationsV2, technologies, investors, acceleratorsV2, keywords, linkedinSlugs, lastFundingUSD, foundedOn and lastFundedOn.
stage or status, is refused with 422 VALIDATION_FAILED. A value outside an open vocabulary, such as an industry or a technology, is not refused: the search comes back empty, so copy values from filter-options. A search with company_filters answers total: null and takes longer than one on people filters alone.
Months in current role
job_start_date finds people by when they started their current role, as a window:
With several values, the earliest start date applies, and
0 adds no filter. A search saved in the app stores this filter as a number of months, so running it finds everyone who started within that many months.
Removed filters
These filters are no longer supported. Sending one in literalfilters to search, save-to-list, or a saved-search create or update is refused with 422 VALIDATION_FAILED before anything is billed, and param names the filter:
education.degrees also loses its exclude option and takes include entries only. An exclude entry is refused with the same 422 VALIDATION_FAILED, with param pointing at the entry, for example filters.education.degrees[0].status.
Filters that come from somewhere other than your request body are dropped instead of refused:
- A saved search (
savedSearchId,savedSearchNameor/run): a removed filter is skipped and its stored key is listed inignoredFilterKeys. Exclude entries oneducation.degreesare dropped, and its include entries still apply. Save-to-list from a saved search skips the same filters, but its202carries noignoredFilterKeys, so run the saved search first and check them before you save from it. - A natural-language
query: the generated filters never use them.
422 SAVED_SEARCH_INVALID for a saved search, 422 QUERY_FILTERS_INVALID for a query.
GET /business/prospects/filter-options no longer lists the removed filters. Rows still carry job_title_sub_role, worked out from the job title, even though you can no longer filter on it.
The department and seniority filters, job_title_role, experience.title.role, job_title_levels and experience.title.levels, take only the values filter-options lists for them. Any other value, such as the retired department real_estate, is refused with 422 VALIDATION_FAILED naming the field and the value, whether it is the only include, one of several, or an exclude, and nothing is billed. Unlike a removed filter, such a value in a saved search is not skipped: running the search answers 422 SAVED_SEARCH_INVALID.
Saved searches
Saved searches let you rerun a query you have already tuned, without carrying the filter JSON around. They are shared with the Fuse app in both directions: a search saved in the app is callable from the API, and a search created through the API opens identically in the app’s filter rail.GET /business/saved-searches lists the searches saved on your account, people and company searches together. Each one’s searchType says which, and its filters are reported in that search’s own vocabulary: people searches in the keys POST /prospects/search accepts, company searches in the keys of POST /companies/search. Add ?type=people or ?type=company to list only one kind.
Create, update, delete
POST /business/saved-searches stores a filter set under a title — the same filters object the search endpoint takes:
201 returns the stored search in the list-row shape above. A filter field that cannot round-trip through the app’s filter rail is rejected with 422 SAVED_SEARCH_FILTERS_UNSUPPORTED (param names the field) rather than stored unusable — run such filters as a direct search instead.
A removed filter, or an exclude entry on education.degrees, is refused with 422 VALIDATION_FAILED on both create and PATCH.
GET /business/saved-searches/{savedSearchId} reads one. PATCH renames it and/or replaces its filters (send title, filters, or both). DELETE removes it and returns a 204:
Run one
The primary way isPOST /business/prospects/search with savedSearchId (or savedSearchName — strict resolve, 409 SAVED_SEARCH_NAME_AMBIGUOUS when two share the title), as shown in step 2. POST /business/saved-searches/{savedSearchId}/run does the same run. Both are for people searches: a company search answers 422 SAVED_SEARCH_TYPE_MISMATCH, so run it with POST /business/companies/search and savedSearchId instead.
ignoredFilterKeys lists any stored filter keys that could not be translated into search fields. This happens with stale saved searches, ones saved against filter names that have since changed, and with searches that use a removed filter, listed under the key in the table’s last column (for example gender or currentJobSubDepartments). Ignored filters are dropped, not approximated, so a non-empty array means the replay ran with fewer constraints than the original search and the results are broader than what you saved. The same goes for exclude entries on education.degrees: they are dropped without being listed, so compare the echoed filters with what you saved.
Check
ignoredFilterKeys before acting on replayed results. If it is non-empty, recreate the search with current filter keys (start from step 1) rather than saving a broader-than-intended result set to a list. Save-to-list with savedSearchId or savedSearchName drops the same keys without reporting them: its 202 has no ignoredFilterKeys.The same loop, for companies
Every step above has a company twin under/business/companies/* (the companies scope): filter-options serves the enum vocabularies (industries, tags, NAICS codes, countries, accelerators, funding stages, statuses, ranking lists), autocomplete resolves company names (+ orgType=investor) and office locations, POST /companies/search takes the same one-of-four filter source and echoes resolved filters + total with cursor paging, and POST /companies/search/save-to-list saves into a company list (create-or-reuse by name). Firmographics land on the saved companies automatically, so there is no enrich switch on the company save. A value off the fiberIndustries, companyTags, fundingStages, statuses, countries, technology, accelerators or fortuneRankings vocabulary is refused with 422 SEARCH_FILTERS_INVALID, whose details.param and details.values name the filter and the entry. Full request/response shapes are in the API reference.
The company save takes the same body as the prospect one: the filters you searched with, a required limit, and listId or listName for the destination. It answers 202 with listIds and fills the list in the background, so follow the list’s completionStatus as in long-running work.
Next steps
Add custom columns
Attach your own fields to the list and fill them per contact.
Export the list
Start an export job, poll it, and download the CSV.
Launch a campaign
Turn the list into a sequenced outreach campaign.
Upload your own contacts
Bring existing contacts into a list instead of searching.