Automate Notion Database Updates with the Public API: Pagination, Rate Limits, and Property Types
Manual updates to Notion databases are slow and error‑prone. By designing a script that respects pagination, rate limits, and property schemas, you can reliably treat Notion as your source of truth. This guide walks through a concrete example and highlights the trade‑offs.
07 Nov 2025, 20:53 UTC

Why Automate Notion Updates?
Teams keep calendars, bug trackers, and hiring lists in Notion because it’s easy to read and share. The catch? Every change is a manual click. When a sprint starts, a dozen rows need status updates, or a hiring manager adds a new candidate. Manual edits are fast for a handful of rows but become a maintenance burden as the data grows. Automating writes turns Notion from a passive viewer into an active system of record that can be kept in sync with external services.
Designing for Reliability
Writing to Notion isn’t just a single HTTP call. The public API exposes a handful of constraints that, if ignored, will cause silent failures or rate‑limit throttling. Below are the three pillars of a robust integration:
- Pagination – Databases can return up to 100 rows per request. The
has_moreflag andnext_cursorfield let you fetch the rest. - Rate Limits – Each integration has a per‑minute cap (≈ 3 k requests). Exceeding it returns a 429; you must retry with back‑off.
- Property Typing – Every property has a JSON schema (title, rich_text, select, etc.). Writing a value that doesn’t match the schema fails.
When you design around these constraints, the integration behaves predictably and can recover from transient errors.
Concrete Example: Updating a Status Column
Assume a database called Bug List with a status select property. We want to set the status of a specific bug to In Review via the API.
Prerequisites
- Integration token from Notion Integrations.
- Database ID (the part after
/database/in the URL). - Page ID of the bug to update.
- Python 3.9+ and
requestslibrary (or any HTTP client). - Pin the API version:
2024-10-01(replace with the latest stable).
Step 1: Query the Database
First, confirm the integration can read the database and fetch the target page. Pagination is handled by looping until has_more is false.
import requests, time
NOTION_TOKEN = "secret_xxx"
NOTION_VERSION = "2024-10-01"
DB_ID = "abcd1234"
PAGE_ID = "efgh5678"
headers = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json"
}
# Build the query payload to filter by page_id if you want a single result
payload = {
"filter": {
"property": "id",
"rich_text": {
"equals": PAGE_ID
}
},
"page_size": 100
}
url = f"https://api.notion.com/v1/databases/{DB_ID}/query"
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
results = response.json()
print(f"Fetched {len(results['results'])} rows")
Check that the returned page contains the status select property. If the integration token wasn’t shared with the database, the results array will be empty.
Step 2: Update the Page
PATCH requests modify a single page. The body must match the property schema exactly. For a select property, the value is an object with a name key matching an existing option.
update_payload = {
"properties": {
"status": {
"select": {
"name": "In Review"
}
}
}
}
update_url = f"https://api.notion.com/v1/pages/{PAGE_ID}"
update_resp = requests.patch(update_url, json=update_payload, headers=headers)
if update_resp.status_code == 429:
# Simple back‑off example – in production use exponential back‑off with jitter
time.sleep(2)
update_resp = requests.patch(update_url, json=update_payload, headers=headers)
update_resp.raise_for_status()
print("Status updated successfully")
After the PATCH, re‑query the page to ensure the change persisted. Remember that formulas and rollups are read‑only; attempting to write them will return a 400 error.
Step 3: Verify Consistency
Notion’s API is eventually consistent for search, but database queries are authoritative. After a write, immediately query the page to confirm the new value. If you need to reconcile a large dataset, run a full sync loop that pulls all rows, compares to your local state, and patches differences.
Trade‑offs and Limitations
- No Bulk Operations – Each row update requires a separate PATCH. For hundreds of rows, you’ll hit rate limits quickly. Batch your updates in chunks and respect the 429 back‑off.
- Read‑Only Fields –
formula,rollup,created_time, and others cannot be written. If your workflow relies on derived data, compute it in your service and store the result in a writable property. - Partial Failures – Because there are no transactions, a failure on one row doesn’t roll back others. Design idempotent updates using a unique external ID property and run a reconciliation pass if needed.
- Version Drift – The API is versioned. If you upgrade the
Notion-Versionheader, endpoints like/databases/{id}/querymay change behavior. Pin the version in production and monitor the changelog. - Token Security – Store the integration token out of the client. A leaked token can read or modify every page the integration is shared with.
Actionable Checklist
- Create an integration and share the target database with it.
- Pin a stable
Notion-Versionheader. - Implement pagination loops that use
has_moreandnext_cursor. - Validate property schemas before writing (use the
propertiesfield in the database schema). - Add retry logic for 429 and 5xx responses with exponential back‑off and jitter.
- After every write, re‑query the affected page to confirm the change.
- Store an external identifier on each page to aid reconciliation.
By following these guidelines, you can turn Notion from a manual spreadsheet into a reliable, automated source of truth that scales with your team’s needs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.