Back to Blog

RedNote (Xiaohongshu) API Quickstart: Your First Request in Python

Rnote API Team · · 256 views · 中文
Xiaohongshu Data API Tutorial Python

This tutorial gets your first RedNote (Xiaohongshu) data API call working in Python in about 5 minutes. No crawler to build, no proxies to maintain — the Rnote API handles the collection infrastructure, so all you do is send a standard HTTP request.

Step 1: Sign up and get an API key

Head to the sign-up page, create an account, and verify your email. You'll get free credits on verification, and you can view and manage your API key in the admin panel.

Step 2: Understand the request shape

Every endpoint is standard REST:

  • Base URL: https://rnote.dev/api/v2/crawler
  • Auth: send X-API-Key: YOUR_API_KEY in the request header
  • Response: JSON

Step 3: Make your first request

Using "search notes" as an example:

import requests

API_BASE = "https://rnote.dev/api/v2/crawler"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}

resp = requests.get(
    f"{API_BASE}/search/notes",
    params={"keyword": "美食推荐", "page": 1},
    headers=HEADERS,
)
print(resp.status_code)
print(resp.json())

Once that works, swap the endpoint for note/image (image-note details), user/info (creator info), and more — see the API docs for parameters.

Step 4: Handle errors and retries

Rnote API returns standard HTTP status codes. A robust helper:

import time
import requests

def call(path, params, retries=3):
    url = f"https://rnote.dev/api/v2/crawler/{path}"
    headers = {"X-API-Key": "YOUR_API_KEY"}
    for i in range(retries):
        r = requests.get(url, params=params, headers=headers, timeout=30)
        if r.status_code == 200:
            return r.json()
        time.sleep(2 ** i)  # exponential backoff
    r.raise_for_status()

The good news: only successful (HTTP 2xx) requests are billed, so retries on failures cost nothing — add retry logic with confidence.

Pricing & credits

  • Billed per request. No monthly fee, no minimum spend — pay for what you use.
  • Only successful requests are charged.
  • Free credits on sign-up, enough to validate the API and run small tests.
  • See top-up options and current offers on the pricing page.

Common errors and how to read them

When something doesn't work, the status code has usually already told you why:

Status Meaning What to do
401 Key missing or invalid Check the header name is X-API-Key (not Authorization), and that the key isn't disabled
402 Insufficient balance Top up. Retrying does nothing — the request never ran and nothing was charged
429 Rate limited Back off and retry; don't spin in place. See concurrency below
404 Wrong path, or the resource is gone Confirm the path in Swagger first; if it's right, the note or account no longer exists
422 Invalid parameters The response body names the offending field — usually a missing required param or a type mismatch

One frequent mistake is putting parameters in the wrong place: the crawler endpoints are GET with query parameters (params=), while the Pugongying endpoints are POST with a JSON body (json=). Mixing them up gives you a 422.

Pagination

Search endpoints page with page, but from page two onward you must pass back the search_id and search_session_id from the first response — otherwise results can repeat:

import requests

API = "https://rnote.dev/api/v2/crawler"
H = {"X-API-Key": "YOUR_API_KEY"}

def search_all(keyword, max_page=5):
    sid = ssid = ""
    seen, out = set(), []
    for page in range(1, max_page + 1):
        r = requests.get(f"{API}/search/notes", headers=H, timeout=30, params={
            "keyword": keyword, "page": page,
            "search_id": sid, "search_session_id": ssid,
        })
        if r.status_code != 200:
            break
        data = r.json()
        sid = data.get("search_id", sid)           # take from the first response, carry it
        ssid = data.get("search_session_id", ssid)
        items = data.get("data", {}).get("items", []) or []
        if not items:
            break                                   # nothing more — stop paying for pages
        for it in items:
            nid = it.get("id")
            if nid and nid not in seen:             # pages overlap; dedupe is mandatory
                seen.add(nid); out.append(it)
    return out

Two things worth remembering: stop on an empty page (paging further just costs money), and dedupe by ID (adjacent pages frequently overlap).

Persist the results

Write to disk instead of printing:

import csv, json, pathlib

notes = search_all("camping gear")

# Keep the raw JSON — you can always re-parse it when you want another field
pathlib.Path("notes.json").write_text(
    json.dumps(notes, ensure_ascii=False, indent=2), encoding="utf-8")

# A spreadsheet for non-technical colleagues
with open("notes.csv", "w", newline="", encoding="utf-8-sig") as f:
    w = csv.writer(f)
    w.writerow(["note_id", "title", "likes"])
    for n in notes:
        w.writerow([n.get("id"), n.get("display_title", ""),
                    n.get("interact_info", {}).get("liked_count", 0)])

The utf-8-sig encoding is for Excel — a plain utf-8 CSV opens as mojibake for non-ASCII titles.

Next steps

Ready? Sign up free and send your first request now.