This script searches Xiaohongshu (RedNote, 小红书) by keyword and writes one CSV row per note: title, type, likes, saves, comments, shares, post date and author ID. We ran it on 2026-10-12 for 咖啡 (coffee), sorted by popularity, and got 58 notes in 7 seconds.
Which API to call
RedNote's own developer platform, open.xiaohongshu.com, is for shop tools such as ERP and order printing, and lists no search API (what it covers). We make ToolzerHub; the script calls our RedNote API instead, which returns public search results as JSON.
What you need
- Python 3 and
requests(pip install requests). - An API key from the dashboard. Each search call is 15 credits, so the free tier's 100 credits a month cover 6 calls, about 120 notes.
The script
import csv
import os
import sys
import time
from datetime import datetime, timezone
import requests
URL = "https://api.toolzerhub.com/v2/rednote/search/posts"
HEADERS = {"x-api-key": os.environ["TOOLZERHUB_API_KEY"]}
FIELDS = ["note_id", "title", "type", "likes", "saves", "comments", "shares",
"posted", "author_id"]
def cell(value):
# Spreadsheets run text that starts with = + - or @ as a formula; an apostrophe keeps it text.
if isinstance(value, str) and value[:1] in ("=", "+", "-", "@", "\t", "\r"):
return "'" + value
return value
def get(params, tries=5):
# 429 and 503 are retryable; the error body says how long to wait.
for attempt in range(tries):
r = requests.get(URL, headers=HEADERS, params=params, timeout=90)
if r.status_code not in (429, 503) or attempt == tries - 1:
r.raise_for_status()
return r.json()["data"]
wait = r.json().get("error", {}).get("details", {}).get("retryAfter", 5 * (attempt + 1))
time.sleep(min(wait, 60))
def row(note):
return {
"note_id": note["id"],
"title": note.get("title") or note.get("desc", "")[:80],
"type": note.get("type"),
"likes": note.get("liked_count"),
"saves": note.get("collected_count"),
"comments": note.get("comments_count"),
"shares": note.get("shared_count"),
"posted": datetime.fromtimestamp(note["timestamp"], timezone.utc).date().isoformat(),
"author_id": (note.get("user") or {}).get("userid"),
}
def search(query, sort, pages):
cursor = None
for page in range(1, pages + 1):
params = {"query": query, "sort": sort, "page": page}
if cursor:
params["cursor"] = cursor
data = get(params)
notes = [item["note"] for item in data["items"] if item.get("model_type") == "note"]
yield from notes
cursor = data.get("next_cursor")
if not notes or not cursor:
break
def main(query, sort="relevance", pages=3, out="notes.csv"):
seen = set()
with open(out, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
writer.writeheader()
for note in search(query, sort, int(pages)):
if note["id"] not in seen:
seen.add(note["id"])
writer.writerow({k: cell(v) for k, v in row(note).items()})
print(f"wrote {len(seen)} notes to {out}")
if __name__ == "__main__":
main(*sys.argv[1:])
Each call returns 20 results and a next_cursor. The loop sends the cursor back with the next page number until there is no cursor or it has read the pages you asked for. It keeps only items whose model_type is note, and skips a note it has already written: our 3 calls returned 60 notes, 2 of them repeats of earlier ones.
cell() puts an apostrophe in front of any text that starts with =, +, - or @, so a spreadsheet shows it as text instead of running it as a formula.
If a note has no title, row() falls back to the first 80 characters of its body text, desc.
Run it
export TOOLZERHUB_API_KEY=your_key
python rednote_search.py 咖啡 popular 3 notes.csv
Output on 2026-10-12: wrote 58 notes to notes.csv, in 7.1 seconds. The first three rows:
| title | type | likes | saves | comments | posted |
|---|---|---|---|---|---|
| 🇪🇸当西班牙的餐后咖啡已经必不可少 | video | 212,491 | 6,557 | 1,346 | 2026-08-05 |
| 我爱的小众咖啡馆不再小众了 | video | 112,702 | 23,433 | 804 | 2026-05-04 |
| 存一些i人小幅度咖啡店拍照姿势>< | video | 66,366 | 47,725 | 216 | 2026-02-12 |
The third note, titled about coffee-shop photo poses, has 47,725 saves against 66,366 likes, a 72% ratio; the first has 3%. Rank rows by saves ÷ likes to find notes that people save more than the rest.
What the 58 notes looked like
- 39 image notes (
typeisnormal) and 19 video notes. The median video had 31,223 likes; the median image note had 6,022. - 58 different authors: no two notes in this search shared an account.
- Post dates ran from 2022-03-12 to 2026-09-22; 40 of the 58 were from 2026.
These numbers describe one search on one day, sorted by popularity, so they are not a sample of RedNote as a whole.
Search parameters
sort:relevance(default),recent,popular,most_commented,most_savedorenglish.- Add
"content_type": "video"(orimage,live) toparamsto filter by format, and"time_range": "week"(orday,half_year) for recent notes only. - One account instead of a keyword: export a RedNote profile's notes.
- English queries work: a separate search for "coffee" returned 20 notes, all with Chinese titles.
Open a note from the CSV
A plain xiaohongshu.com/explore/<note_id> link showed no note content when we opened it logged out, so the CSV keeps the ID instead of a URL. Pass note_id to /v2/rednote/posts/video (for type video) or /v2/rednote/posts/image (for normal) for the full note, or to /v2/rednote/posts/comments for its comments.
What it costs
Every RedNote v2 call is 15 credits. This run was 3 calls, 45 credits. A search page holds 20 notes, so 1,000 notes take 50 calls, 750 credits: $0.74 to $1.43 at our rates of $0.99 to $1.90 per 1,000 credits.
We are not affiliated with RedNote.
Sources
- RedNote e-commerce open platform, https://open.xiaohongshu.com/, read 2026-10-12
- ToolzerHub RedNote reference (search parameters), https://docs.toolzerhub.com/reference/rednote/search, checked 2026-10-12
- ToolzerHub pricing, https://toolzerhub.com/pricing, checked 2026-10-12