Filtering and Sorting Notion Databases via the API for Custom Dashboards
Learn how to filter and sort Notion database records via the API to build efficient custom dashboards, with a worked curl example, pagination handling, and rate‑limit considerations.
20 Jun 2026, 00:18 UTC

When you build a dashboard that pulls data from Notion, you often need only a slice of a database—such as tasks that are both “In Progress” and due within the next week. Fetching the whole database and filtering locally wastes bandwidth and makes the integration slower. The Notion API’s POST /v1/databases/{database_id}/query endpoint lets you push the filtering and sorting work to Notion’s servers, returning exactly the rows you need.
1. Crafting the filter JSON
The request body is a JSON object with two key sections: filter (defines conditions) and sorts (orders results). Each filter targets a specific property type and uses an operator. For our example we need:
- A
selectfilter on the “Status” property that equals"In Progress". - A
datefilter on the “Due Date” property that falls within the next seven days.
These two conditions are combined with an implicit AND, so both must be true. The JSON looks like this:
{
"filter": {
"and": [
{
"property": "Status",
"select": {
"equals": "In Progress"
}
},
{
"property": "Due Date",
"date": {
"on_or_after": "{{today}}",
"on_or_before": "{{next_week}}"
}
}
]
},
"sorts": [
{
"property": "Due Date",
"direction": "ascending"
}
]
}
Replace {{today}} and {{next_week}} with ISO‑8601 strings (e.g., 2026-10-13 and 2026-10-20) generated by your script.
2. Handling pagination and rate limits
The query returns up to 100 rows per call. If more rows match, the response includes has_more: true and a start_cursor value. Your client must loop, sending the cursor back in the next request’s start_cursor field until has_more is false.
Notion enforces a rate limit of three requests per second per integration. Exceeding it yields HTTP 429. A simple back‑off strategy—waiting one second and retrying—keeps you within the limit without complex throttling logic.
3. Worked example with curl
Assume you have:
- A Notion database ID:
DATABASE_ID - An internal integration token with the
read_contentscope:TOKEN
The following Bash snippet demonstrates the full loop:
#!/usr/bin/env bash
DATABASE_ID="DATABASE_ID"
TOKEN="TOKEN"
NEXT_CURSOR=""
while true; do
# Build the JSON payload; insert the cursor if we have one
PAYLOAD=$(cat <&2
if [[ "$HTTP_STATUS" -eq 429 ]]; then
echo "Rate limited – sleeping 2s" >&2
sleep 2
continue
else
break
fi
fi
# Process the page of results (e.g., extract IDs)
echo "$BODY" | jq -r '.results[].id'
# Check for more pages
HAS_MORE=$(echo "$BODY" | jq -r '.has_more')
if [[ "$HAS_MORE" != "true" ]]; then
break
fi
NEXT_CURSOR=$(echo "$BODY" | jq -r '.next_cursor')
# Respect rate limit: pause briefly between pages
sleep 0.4
done
This script:
- Builds a filter for Status = “In Progress” and Due Date within the next week.
- Sorts results by Due Date ascending.
- Adds a
start_cursorwhen paging. - Checks the HTTP status, handles 429 with a short sleep, and exits on other errors.
- Prints each matching page’s record IDs (you could replace this with any processing logic).
- Loops until
has_moreis false, pausing 400 ms between pages to stay under the 3‑req/s limit.
4. Trade‑offs and limitations
While pushing filter logic to Notion reduces data transfer, there are practical constraints:
- Payload size: The filter JSON must stay under 1 000 characters. Deeply nested AND/OR trees can exceed this limit; in such cases, split the query into multiple simpler requests and merge results client‑side.
- Rate limit: Three requests per second is low for high‑frequency dashboards. If you need near‑real‑time updates, consider caching results for a short interval (e.g., 30 seconds) and refreshing in the background.
- Property support: Not all property types expose every operator (e.g.,
datedoes not have acontainsoperator). Consult the API reference for the exact capabilities of each type.
You can verify that your filter works as expected by:
- Creating a test database with a “Due Date” (date) and “Status” (select) property.
- Adding a handful of rows that span various statuses and dates.
- Running the
curlrequest manually (or via the script) and inspecting the JSON response. - Confirming that only rows with Status = “In Progress” and a Due Date within the next week appear, and that they are ordered by Due Date.
- If you add more than 100 matching rows, ensure the response includes
has_more: trueand that a second request with the returnedstart_cursoryields the remaining rows.
Actionable closing
To start using this pattern in your own integration:
- Enable the
read_contentscope for your internal integration and copy the token. - Identify the database you want to query and note its ID.
- Implement the filter‑and‑sort JSON structure shown above, adapting the property names and operators to your schema.
- Wrap the request in a loop that handles
start_cursorand respects the 3‑req/s rate limit with simple back‑off. - Test with a small dataset, then scale up, watching for payload‑size or rate‑limit errors and adjusting the query or caching strategy as needed.
By moving the filtering and sorting work into the Notion API, you keep your dashboard lightweight, responsive, and driven by the most relevant data.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.