Endpoints

Filter values

List the values a filter can be set to — the lookup behind an autocomplete or a filter picker, for company/person filters and for each document category's own.

GET/v1/search/filters/{name}

List the values a filter can take

The values one filter accepts — for autocomplete and pickers.

Geography is the one place the name here differs from the name you filter by. geo spans four levels and has no single vocabulary, so it is not a name this route accepts. Enumerate places one level at a time (country, state, city, region) and feed the answer back as geo.places[].value with that same level.

Two-level filters. industry and department are parent/child. Ask for the rows with level=parents, then one branch at a time with parent=<name> as the user expands it. department cannot be done any other way: its plain value list is all 248 values of both levels with nothing marking which is which. level=children gives the other rung, de-duplicated.

Scope: dsp.search.read

Use it when you are populating a filter picker or autocomplete, or checking that a value exists in the index before searching on it. Outcome: the canonical values the named filter accepts.

Paired with the filters object on POST /v1/search:

  • filters on search says which filters exist.
  • This route says what they can hold.

Between them you can build a filter UI without hard-coding a vocabulary that drifts the moment the index does. For what each filter means and which shape its value takes, see the filters guide.

Request#

searchstring | null

Case-insensitive substring match, applied before paging. ≥ 1 chars, ≤ 200 chars.

pageintegerdefault: 1

1-indexed. Page size is fixed at 20. ≥ 1.

parentstring | null

One parent's children, for the two-level filters (industry, sub_industry, department). How a tree picker fills a branch on expand — the child list carries no parent of its own, and no substring search stands in for one. Composes with search and page. Mutually exclusive with level. ≥ 1 chars.

levelstring | null

One rung of a two-level filter: parents or children. parents is a tree's first call, and department needs it — that filter's value list mixes both levels with nothing marking which is which, so the parents cannot be sliced out client-side. Mutually exclusive with parent. pattern ^(parents|children)$.

Path parameters

nameFilterNamerequired

Which filter's values to list.

name is the path parameter, not in the table above. It is a closed set of 54 values:

  • every company/person filter except geo
  • plus the four geo levels country, state, city and region

An unrecognised name is a 422 listing the accepted ones, which is why this route has no 404. geo is refused along with them.

Geography is the one name that differs from the one you filter by

geo spans four levels and has no single vocabulary, so it is not a name this route accepts. Enumerate places one level at a time — country, state, city, region — and feed the answer back as geo.places[].value with that same level. See the geo filter.

Two-level filters: level and parent

industry, sub_industry and department are parent/child, and level and parent are how a tree picker walks them. They are mutually exclusive:

CallReturns
?level=parentsThe top rung — a tree's first call
?parent=Software%20%26%20TechnologyOne parent's children, for the branch a user just expanded
?level=childrenThe other rung whole, de-duplicated

department cannot be done any other way: its plain value list is all 248 values of both levels with nothing marking which is which, so the parents cannot be sliced out client-side. parent composes with search and page.

Response#

filterFilterName

Filter names whose values can be looked up. Every field of DiscoverFilters except geo, plus the four per-level geo names. geo is a request-side directive spanning all four levels and has no value list of its own; the four levels below are how you enumerate places, and each answer belongs in geo.places[].value with the matching level. Upstream 404s on geo for exactly this reason. A closed set rather than a validator: FastAPI turns it into a 422 that lists the accepted names and an OpenAPI enum callers can read, and neither of those is something a hand-written check gives you. Kept explicit rather than derived from DiscoverFilters.model_fields so it stays readable in the generated schema; tests/test_filters_schema.py enforces that the two never diverge.

has_moreboolean
itemsarray of FilterValueItem

Aligned with values, for the filters that have more than a name. Empty for the rest — read it when present, fall back to values.

domainstring | null
idstring | null
namestring | null
valuestring
metaResponseMeta

Present on every 2xx, including the ones that are not paginated.

as_ofstring
credential"api_key" | "session"default: api_key

Which credential produced this response. session means the dashboard playground ran it as the signed-in user rather than as an API key — so it is billed to the user, subject to interactive rate limits, and carries no integration compatibility guarantee. Additive and defaulted, so it breaks no existing client.

credits_chargednumber | null

What this request cost, in credits, to at most three decimals. 0 when it cost nothing: a Basic or AI search that found nothing, rows this search already paid for (a cursor sent again, or a request repeated with the same Idempotency-Key on the same UTC day), or a test key. A request without a cursor or a repeated key is a new search, so keep the first page rather than asking for it again. null on a free endpoint.

environment"live" | "test"

Echoed on every response so a developer can never mistake a test-key result for a live one.

request_idstring
pageinteger
page_sizeinteger

Always 20 — set upstream, not a parameter here.

totalinteger

Matches for search, or the whole vocabulary without one.

valuesarray of string

Empty for a filter with no fixed vocabulary — every range, and the free-text and entity filters. That is an answer, not a failure: those take values you supply.

Not every filter has a vocabulary

An empty values is an answer, not a failure. Three behaviours, all deliberate:

ClassFiltersWith no search
Boundedtechnology, skills, city, state, company_keyword, sic, naicsReturns the most common values immediately
Free-text-backedcompany, title, hiring_role, hiring_locationReturns [] until you set search
No vocabularyperson, employment_history, certifications, universityAlways [] — use plain text entry and skip the call

Every range filter (employee_count, revenue, founded_year, tenure, years_worked, …) and every date filter also returns []: you supply the bounds, there is nothing to enumerate.

Because the second class returns nothing until you type, a picker built on it needs a way to add the typed term verbatim.

items — the rows that need more than a name

items is aligned with values and is populated only where the corpus has something to add. Today that is company alone:

GET /v1/search/filters/company?search=stripe

"values": ["stripe.com", "stripesgraphics.co.uk", …],
"items":  [{ "value":  "stripe.com",
             "name":   "stripe",
             "domain": "stripe.com",
             "id":     "e0391674ac7f4f7ebc8d4ce27ea5d5eb" }, …]

Read items when it is non-empty and fall back to values otherwise. Render the label as name (domain) — a picker showing bare domains asks someone to choose between stripe.com and stripesgraphics.co.uk on the strength of the domain alone.

Document categories have their own route#

GET/v1/search/filters/{category}/{name}

List the values a document filter can take

The values one filter accepts on one document category.

Separate from /search/filters/{name} because the two namespaces collide: employment_type is five Title-Case values on a person and ten snake_case ones on a job posting. Naming the category is what keeps them apart, and it is why name here is a free string rather than the FilterName enum — that enum mirrors the company/person set, and mirroring a second one per category is exactly the duplication this route exists to remove.

Use /search/filters/{name} for company and person.

A filter that exists but takes free values returns an empty list rather than an error — "this one accepts anything" is an answer. A 422 means the name is not a filter on that category at all.

Scope: dsp.search.read

Use this route for the seven document categories, and /v1/search/filters/{name} for company and person. name is a free string, spelled exactly as the search request spells it; the summary above explains why the two namespaces are kept apart.

curl "https://api.sproutsdata.ai/v1/search/filters/{category}/{name}?search=full" \
  -H "X-API-Key: $SPROUTS_API_KEY"
searchstring | null

Case-insensitive substring match, applied before paging. ≥ 1 chars, ≤ 200 chars.

pageintegerdefault: 1

1-indexed. Page size is fixed at 20. ≥ 1.

Path parameters

categoryEntityCategoryrequired

Which document category the filter belongs to.

namestringrequired

The filter name, exactly as the search request spells it.

categoryEntityCategory

*What* to search, and what a returned row is. One parameter carries both, because they are the same fact: a search of patent returns patents. The alternative — a source/vertical selector beside a narrower category — lets a caller ask for patents from the people index, a combination that has no meaning and would need rejecting at runtime. A closed set that cannot express the contradiction is better than a validator that catches it. Values are exactly prism's own row types, so category in the request and results[].category in the response are always the same string. company and person are the GTM index; the rest are separate corpora with their own filters, freshness and coverage. Not everything applies everywhere: see SearchRequest, which rejects the GTM-only parameters rather than ignoring them. job_opening is a document corpus like patent/news_article/etc — a posting, not an entity — but its records carry a domain (the posting company's) all the same. It is deliberately not in GTM_CATEGORIES (that set is about the entity/document distinction, and a job posting is still a document), but it *is* in DOMAIN_FILTERABLE_CATEGORIES, which exists specifically to separate those two axes.

filterstring
has_moreboolean
metaResponseMeta

Present on every 2xx, including the ones that are not paginated.

as_ofstring
credential"api_key" | "session"default: api_key

Which credential produced this response. session means the dashboard playground ran it as the signed-in user rather than as an API key — so it is billed to the user, subject to interactive rate limits, and carries no integration compatibility guarantee. Additive and defaulted, so it breaks no existing client.

credits_chargednumber | null

What this request cost, in credits, to at most three decimals. 0 when it cost nothing: a Basic or AI search that found nothing, rows this search already paid for (a cursor sent again, or a request repeated with the same Idempotency-Key on the same UTC day), or a test key. A request without a cursor or a repeated key is a new search, so keep the first page rather than asking for it again. null on a free endpoint.

environment"live" | "test"

Echoed on every response so a developer can never mistake a test-key result for a live one.

request_idstring
pageinteger
page_sizeinteger

Always 20 — set upstream, not a parameter here.

totalinteger

Matches for search, or the whole vocabulary without one.

valuesarray of string

Empty for a filter with no fixed vocabulary — every range, and the free-text and entity filters. That is an answer, not a failure: those take values you supply.

What neither route covers#

  • The filter list itself. There is no route that enumerates the filters. The typed set is published in the OpenAPI schema as DiscoverFilters, and the 54 lookup names as the FilterName enum, so a generated client gets both for free. The document categories' own sets are small, fixed, and listed in the filters guide.
  • Sort fields. Ordering has its own catalogue — GET /v1/sorts/{category}.

Errors#

StatusCodeSummary
401auth.unauthorizedNo usable credential (cause deliberately unspecified)
403auth.browser_originA secret key was used from a browser
403auth.insufficient_scopeThe credential does not hold the required scope
422pagination.cursor_invalidThe cursor could not be read (e.g. a filter changed mid-pagination)
422request.invalidA field failed validation (extra.errors has the detail)
503credits.unavailableThe billing service could not confirm the charge; retry with the same Idempotency-Key
503upstream.unavailableThe retrieval engine is unreachable (not charged)
504upstream.timeoutThe retrieval engine did not answer in time (not charged)