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#
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.
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.
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, person | the other seven | |
|---|---|---|
| Typed by | This API, end to end | The retrieval engine, one hop away |
| An unknown key | 422 naming the field — filters.industry.includ | 422 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.
| Shape | Fields | Rules |
|---|---|---|
TermsFilter | include: string[], exclude: string[], match_all: boolean | At least one of include/exclude. match_all: true is AND across include; the default is OR |
RangeFilter | gte, lte — numbers, either omittable | At least one bound. gte must not exceed lte. Both inclusive |
DateBound | gte, lte — ISO-8601 (2024-01-01) or date math (now-30d) — or preset | preset xor explicit bounds, never both. Both bounds inclusive; there is no gt/lt |
DateFilter | period: DateBound, last_updated: DateBound | Document categories only. period is when the event happened, last_updated is record freshness. At least one; ANDed when both are set |
EntityFilter | include: EntityRef[], exclude: EntityRef[] | At least one entry |
EntityRef | value: 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 |
TextFilter | include: string[], exclude: string[], exact: boolean | company/person only. exact: true matches the raw stored value instead of an analysed phrase |
KeywordQuery | positive?: { 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 |
KeywordSet | keywords?: 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 kind | Vocabulary | How you populate a picker |
|---|---|---|
| Bounded terms | Yes, a top slice up front | GET /v1/search/filters/{name} |
| Free-text-backed terms | Yes, but only once you send search | The same route, plus an "add what I typed" affordance |
person, employment_history, certifications, university | None, ever | Free entry — skip the call |
| Ranges and dates | None by design | Your own controls |
| Entity references | Resolved server-side | Send { "value": "…" } and omit by |
Two-level terms (industry, sub_industry, department) | Yes, one rung at a time | The same route with level=parents, then parent=<name> |
| Document-category terms | Yes, on the category's own route | GET /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
sortclause on the search request, with its own catalogue atGET /v1/sorts/{category}. - Across categories. A
patentsearch cannot filter on the applicant's employee count:applied_by_companyresolves which company, not that company's attributes. Run acompanysearch first and pass the resulting domains intoapplied_by_company,companiesorstartups. - Both-of on investors.
funding_round.investorsis any-of only. - Named countries on
global_presence. It takes a count and a headcount floor; to name places, usegeo.