Skip to main content
Search is how you find entities you cannot name yet. Every entity has a search endpoint that takes the same request shape, so learning one teaches you all five.

Why POST

Search takes a JSON body rather than query parameters. Filter sets are nested and open-ended — ranges, arrays, several keys at once — and URL-encoding them produces requests that are long, fragile and miserable to debug. A body keeps a filter set readable, diffable and storable as the same JSON you can replay into a tracker later.
These POSTs are read-only and have no side effects. They are safe to retry, and they do not need an Idempotency-Key.

Range filters

Numeric and date filters take a range object. Combine keys to close a range on both sides.
Filters combine with AND. Array filters like category_ids are OR within the array — the example below means “beauty or wellness, priced 15 to 45”.
There is no OR across different filter keys. When you need one, run two searches and merge on id — it is clearer than a query language nobody can read six months later.

Sorting

sort takes up to three keys, applied in order.
The second key breaks ties on the first, which matters more than it sounds: ranking purely on a growth rate floats tiny shops that went from 80to80 to 400. A revenue floor in filters plus revenue as a tiebreaker gives a list a human would agree with.
Sorting is stable across pages for a given cursor. Changing sort invalidates a cursor — start a new search rather than paging on with the old one.

Pagination

Responses are cursor-paginated. Read next_cursor and stop when has_more is false.
Never construct a cursor yourself, and never assume it encodes an offset. See Pagination for cursor lifetime and page-size trade-offs.

Result depth

A search pages to a depth of 1,000 rows. Past that, has_more is false even when more entities match — total_count tells you how many there were. This is a real limit, not a tuning knob: ranked commerce data is served as a ranking, and depth beyond the top of it is not meaningfully ordered.
A revenue floor or a tighter price band usually turns 40,000 matches into the few hundred you actually wanted. This is the right fix most of the time.
Split by category, market or period and union the results. Each shard gets its own 1,000 rows, and sharding by leaf category is normally enough to cover a whole market.
An export job shards for you and returns CSV or JSONL. Use it for anything that feeds a warehouse.

Zero results

An empty data array with total_count: 0 is a finding, not an error — and it is free. Zero- result searches cost no credits. If a search returns unexpectedly little, check these in order:
1

Is the market entitled?

An unentitled market returns coverage_error, not an empty list. If you got a 200, this is not the problem.
2

Is every filter field filterable here?

A partial field that is filterable will silently exclude entities where it is missing. Check GET /coverage.
3

Was the period adjusted?

If period.adjusted is true, you measured a wider window than you asked for, and your growth filters mean something slightly different.
4

Are the thresholds in the right currency?

"revenue": { "gte": 100000 } is 100,000 units of your requested currency. In IDR that is a very different bar than in USD.