HubSpot Search API: How to Filter, Sort, and Paginate Without the Headaches
There is a moment everyone who works with the HubSpot API lives through. You do not want all the contacts, you want only the ones that match a condition. The deals in negotiation. The companies in one industry. The contacts that changed since yesterday. And then comes the temptation to pull the whole database and filter it in your own code, on your side.
Do not do that. There is a right way, and it is called the Search API. Instead of downloading tens of thousands of records to discard most of them, you send the filter to HubSpot and get back only what matters. Less data on the wire, less memory consumed, less processing time and, most importantly, less risk of hitting the portal's rate limits.
But the Search API has its own rules, and whoever ignores them runs into the famous "the search stops at 10,000 and nobody knows why". In this guide, I will show you everything: how to build the filter, which operators exist, how to sort, how to paginate and, most importantly, the trick to extract databases larger than the cap. By the end, you will write searches that cover the whole base without losing a single record.
What the Search API is and when to use it

The Search API is the endpoint that returns a subset of records according to the filters, sorting, and property selection you define. It is a POST, not a GET, because the filter goes in the request body. Use it whenever you need a slice, and leave the simple listing for when you need absolutely everything.
This choice between listing and searching seems like a detail, but it defines the health of your integration. Searching when you should list locks you into the 10,000 cap. Listing when you should search makes you move the entire base just to use 2% of it. The table below sums up the decision.
|
You need... |
Use |
Why |
|---|---|---|
|
The whole base of an object |
Listing (GET .../objects/{type}) |
Simpler and no 10,000 cap |
|
A slice by date, stage, or value |
Search (POST .../search) |
Filters on the server, brings only what is needed |
|
Records whose ids you already have |
Batch read |
No need to filter, you already know who you want |
Why this matters for your operation
Before the code, the why. Well-built searches are the foundation of nearly every revenue process that depends on fresh data. An alert for a stalled deal, a pipeline report by stage, a routine that syncs only the contacts that changed: each of these is a Search API behind the scenes. When the search is wrong, the symptom does not show up in the code, it shows up in the number. The dashboard shows a smaller pipeline than the real one, the alert does not fire, the report goes missing with part of the base. And then the team loses trust in the data, which is the most expensive asset a sales operation has.
How to build a search
A search is structured in filterGroups. Within a single group, filters combine with AND logic. Different groups combine with OR logic. This is the key to building complex conditions. "Deals in negotiation AND above one thousand" is one group with two filters. "In negotiation OR already won" is two groups, each with one filter.
|
POST /crm/v3/objects/deals/search { "filterGroups": [ { "filters": [ { "propertyName": "dealstage", "operator": "EQ", "value": "appointmentscheduled" }, { "propertyName": "amount", "operator": "GTE", "value": "1000" } ] } ], "properties": ["dealname", "amount", "dealstage"], "limit": 100 } |
Notice the properties. Ask only for the fields you will actually use. Each extra property bloats the response and brings you closer to the rate limits. Whoever asks for everything "just in case" pays dearly in performance and gains little in return, because when it comes time to use it they almost never need all of that.
The operators that solve 90% of cases
- `EQ` and `NEQ`: equal and not equal. The bread and butter of filters.
- `GT`, `GTE`, `LT`, `LTE`: greater, greater or equal, less, less or equal. They work for numbers and for dates.
- `BETWEEN`: a closed interval, great for date windows of creation or modification.
- `IN` and `NOT_IN`: inside or outside a list of values, like selecting several stages at once.
- `HAS_PROPERTY` and `NOT_HAS_PROPERTY`: the field has or does not have a value, regardless of which value it is.
Dates use epoch milliseconds or ISO format. A silent and very common mistake is sending the date in seconds: HubSpot works in milliseconds, so multiply by a thousand if your source is in seconds. When a date filter "brings nothing" or "brings everything", suspect the unit before anything else.
Selecting only the right properties
It is worth repeating because it is the highest-return tweak: list in properties only what the task consumes. If you sync name and email to a BI tool, do not bring twenty fields. The smaller response travels faster, uses less memory, and gives you more headroom on the rate limit, which matters a lot when the base is large.
Sorting: the basis of a predictable sweep
The sorts field orders the result. To sweep records predictably, sort by hs_lastmodifieddate or createdate in ascending order. This matters more than it seems. A stable order is what keeps records from jumping pages while you paginate, which is one of the subtlest causes of lost or duplicated records in long extractions.
|
"sorts": [{ "propertyName": "hs_lastmodifieddate", "direction": "ASCENDING" }] |
Pagination in search, step by step
Search paginates by cursor, just like simple reads. The response carries paging.next.after, and you repeat the call with that value in after until the cursor disappears. The golden rule: on the first call, do not send after. Some endpoints reject an empty after, so omitting it on the first page is the safe path in every scenario.
|
A tip from someone who has been burned: if your search keeps returning the same records in a loop, the cursor is almost always not being passed between calls. Check that you are reading the after from the previous response and injecting it into the next request, instead of resending the first body over and over. |
The 10,000 cap and how to get past it
Here is the rule that catches the most people off guard. The search returns at most 10,000 records per filter set. If your condition matches more than that, pagination simply stops at the limit, without a clear error shouting at you. You think you extracted everything, but half the base is missing, and nobody notices until the number diverges down the line.
The solution is not to insist on the same search, it is to divide. Break the query into ranges that do not overlap, for example by creation date windows, and paginate each range separately. January, February, March, each month as an independent search. Each range stays below the cap, and the sum of the ranges covers the whole base. For very dense bases, use smaller windows, by week or by day.
The second limit, the rate limit
The Search API has its own rate limit, stricter than simple reads. If your routine does many searches in sequence, that is where the 429 shows up first. Handle it with exponential backoff, respecting the header that tells you when to retry, and consider spacing the calls out. Searching in ranges helps here too, because it gives the routine a rhythm instead of slamming everything at once.
Common mistakes that cost hours

- Date in seconds instead of milliseconds: the filter looks broken, but it is just the wrong unit.
- Not passing the cursor: the extraction loops or stops early.
- Ignoring the 10,000 cap: HubSpot returns a 400 error when you try to page past 10,000, and code that does not handle it stops halfway without anyone noticing.
- Asking for too many properties: heavier, slower responses and more data to process.
- Searching when you should list: you lock yourself into a cap you did not need to face.
In practice: the report that looked incomplete
An operation extracted deals into a pipeline dashboard and swore data was missing. The search used a single broad filter and always stopped near 10,000 records, hitting the cap without anyone noticing. The dashboard consistently showed a smaller pipeline than reality, and sales leadership distrusted the number at every forecast meeting.
By breaking the extraction into creation date windows and paginating each one, the dashboard came to reflect the entire base. The forecast lined up with the operation again. The problem was never the dashboard nor the BI tool, it was the way of searching. It is the kind of invisible fix that restores trust in the data.
Checklist before running a search at volume
- Did you ask only for the necessary properties in properties?
- Are the filters grouped correctly, AND within the group and OR between groups?
- Are the dates in milliseconds, not seconds?
- Is there a sorts by a stable field, so the sweep is predictable?
- Does the search fit below 10,000, or is it broken into non-overlapping ranges?
- Is there 429 handling with exponential backoff?
- Do you validate the extracted total against the expected one at the end?
Frequently asked questions
What is the difference between the Search API and a simple list?
Listing (GET /crm/v3/objects/{type}) brings all records in a fixed order and has no 10,000 cap. Search (POST .../search) accepts filters and sorting and returns only the matching subset. Use listing for full extraction and search for slices.
Why does my search stop at 10,000 records?
It is the Search API cap per filter set. Do not insist on the same search: split the query into non-overlapping ranges, for example by creation date windows, and paginate each one separately.
How do I combine AND and OR conditions in a search?
Filters within the same filterGroup combine with AND logic. Different groups combine with OR logic. For "A and B", one group with two filters. For "A or B", two groups with one filter each.
Does search count against the same rate limit as reads?
No. The Search API has its own, lower limit. Routines with many searches hit the 429 before simple reads, so handle it with backoff and space the calls out.
Why does my date filter bring nothing?
Almost always the unit. HubSpot expects epoch in milliseconds. If your source is in seconds, multiply by a thousand before sending the filter value.
Can I sort by any property?
Yes, but for sweeps the safest is to sort by a stable field like createdate or hs_lastmodifieddate, ascending, so records do not jump pages during pagination.
Need to extract specific slices of your CRM without building it all by hand or stopping at 10,000? At Insight Sales, as a HubSpot partner, we build data routines that respect the limits and run safely. Talk to our team and find out how we can help.
Ready to take your operation to the next level?
Talk to a specialist and see how we can help.