Guides

Filters

Every filter POST /v1/search accepts, per category — value shapes, the nesting that trips people up, and where a filter's values come from.

Filters are how you say exactly what you mean. A natural-language query asks the engine to interpret your words; a filter states the constraint directly ("industry is one of these", "between 500 and 5,000 employees", "headquartered in Germany") and is applied exactly as sent, with no interpretation in the path. Every precise segment you build starts here.

filters is optional, but a request must carry query or filters or both. A filters-only search is a first-class intent and costs nothing extra: with no sentence to parse, the response comes back with resolved_search.parser_version: "none".

Use cases#

Making a segment repeatable

A natural-language search found the right companies once; now it must be reproducible every week. Re-send resolved_search.filters as explicit filters with no query. You get a deterministic request (same filters, same segment, exact total) for saved audiences and scheduled jobs.

Powering filter controls in a UI

Your product needs dropdowns for industry, country, or funding stage. Call GET /v1/search/filters/{name} (free) and render the values. You get options that can never produce an impossible query, because every choice is a value the index holds.

Narrowing a search that returns too much

A broad query matches tens of thousands of rows. Add one constraint at a time (a geography, a size band, an industry) and watch total contract. You get a segment small enough to page through completely.

Which filters exist depends on category. This page is the full reference for all nine. For the endpoint that tells you what a filter can be set to, see filter values.

Two validation paths, one field name#

The single most important fact about filters, and easy to miss because both paths use the same key:

company, personthe other seven
Typed byThis API, end to endThe retrieval engine, one hop away
An unknown key422 naming the field — filters.industry.includ422 without the path, just "extra inputs are not permitted"

Either way the request is refused before it is charged, and either way you send the filters under filters. Only the error detail differs.

Value shapes#

Seven reusable shapes cover almost every filter. They validate identically wherever they appear: a RangeFilter on employee_count and one on funding_size behave the same. What varies per category is which fields exist and which shape each one uses.

ShapeFieldsRules
TermsFilterinclude: string[], exclude: string[], match_all: booleanAt least one of include/exclude. match_all: true is AND across include; the default is OR
RangeFiltergte, lte — numbers, either omittableAt least one bound. gte must not exceed lte. Both inclusive
DateBoundgte, lte — ISO-8601 (2024-01-01) or date math (now-30d) — or presetpreset xor explicit bounds, never both. Both bounds inclusive; there is no gt/lt
DateFilterperiod: DateBound, last_updated: DateBoundDocument categories only. period is when the event happened, last_updated is record freshness. At least one; ANDed when both are set
EntityFilterinclude: EntityRef[], exclude: EntityRef[]At least one entry
EntityRefvalue: string, by?: "name" | "domain" | "li" | "id"Omit by and the server classifies the token — it reads a domain out of a name better than a picker that only knows which list the value came from
TextFilterinclude: string[], exclude: string[], exact: booleancompany/person only. exact: true matches the raw stored value instead of an analysed phrase
KeywordQuerypositive?: { sets: KeywordSet[], relation }, negative?: { sets, relation }Document categories only, and not on every one. At least one of the two groups. relation is AND or OR
KeywordSetkeywords?: string[], qualifiers?: string[]At least one array. Entries within an array are OR'd; keywords and qualifiers must co-occur when both are present

preset is one of last_7_days, last_30_days, last_90_days, last_6_months, last_year, last_3_years, last_5_years.

Where a filter's values come from#

Filter kindVocabularyHow you populate a picker
Bounded termsYes, a top slice up frontGET /v1/search/filters/{name}
Free-text-backed termsYes, but only once you send searchThe same route, plus an "add what I typed" affordance
person, employment_history, certifications, universityNone, everFree entry — skip the call
Ranges and datesNone by designYour own controls
Entity referencesResolved server-sideSend { "value": "…" } and omit by
Two-level terms (industry, sub_industry, department)Yes, one rung at a timeThe same route with level=parents, then parent=<name>
Document-category termsYes, on the category's own routeGET /v1/search/filters/{category}/{name}

A vocabulary is a shortcut, never a gate. An unrecognised free-text value is passed through rather than rejected, and simply matches nothing, which is visible in the result count.

What you cannot filter by#

  • Sorting. Ordering is not a filter: it is the sort clause on the search request, with its own catalogue at GET /v1/sorts/{category}.
  • Across categories. A patent search cannot filter on the applicant's employee count: applied_by_company resolves which company, not that company's attributes. Run a company search first and pass the resulting domains into applied_by_company, companies or startups.
  • Both-of on investors. funding_round.investors is any-of only.
  • Named countries on global_presence. It takes a count and a headcount floor; to name places, use geo.

More on filters#