Back to Blog

RedNote (Xiaohongshu) API Quickstart with Go

Rnote API Team · · 134 views · 中文
Xiaohongshu Data API Tutorial Go

This tutorial connects you to the RedNote (Xiaohongshu) data API with Go. Like the Python and Node.js versions, you build no crawler — the Rnote API handles collection infrastructure, and you just send a GET request with the standard net/http package.

Step 1: Sign up and get an API key

Go to the sign-up page, create an account, verify your email, and get free credits plus your API key.

Step 2: Make your first request

Standard library only — no third-party dependencies:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
)

func main() {
    endpoint := "https://rnote.dev/api/v2/crawler/search/notes"
    q := url.Values{}
    q.Set("keyword", "camping gear")
    q.Set("page", "1")

    req, _ := http.NewRequest("GET", endpoint+"?"+q.Encode(), nil)
    req.Header.Set("X-API-Key", "YOUR_API_KEY")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    var data map[string]any
    json.Unmarshal(body, &data)
    fmt.Println(data)
}

Step 3: Wrap a call with retries

func call(path string, params url.Values, retries int) ([]byte, error) {
    endpoint := "https://rnote.dev/api/v2/crawler/" + path + "?" + params.Encode()
    var lastErr error
    for i := 0; i < retries; i++ {
        req, _ := http.NewRequest("GET", endpoint, nil)
        req.Header.Set("X-API-Key", "YOUR_API_KEY")
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            lastErr = err
        } else {
            body, _ := io.ReadAll(resp.Body)
            resp.Body.Close()
            if resp.StatusCode == 200 {
                return body, nil
            }
            lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
        }
        time.Sleep(time.Duration(1<<i) * time.Second) // exponential backoff
    }
    return nil, lastErr
}

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; free credits on sign-up. See top-up options and current offers on the pricing page.

Bounding concurrency with a buffered channel

The classic mistake when writing a crawler in Go is spawning a goroutine directly inside the loop — hundreds of requests leave at once and you reliably collect a wall of 429s. A buffered channel used as a semaphore fixes it:

package main

import (
    "fmt"
    "sync"
)

func fetchAll(ids []string) map[string][]byte {
    const concurrency = 4          // match your key's rate limit; don't get greedy

    sem := make(chan struct{}, concurrency)
    var wg sync.WaitGroup
    var mu sync.Mutex
    out := make(map[string][]byte, len(ids))

    for _, id := range ids {
        wg.Add(1)
        go func(noteID string) {
            defer wg.Done()
            sem <- struct{}{}        // take a slot; blocks here when full
            defer func() { <-sem }() // always give it back

            body, err := getNote(noteID)   // your retry-wrapped call
            if err != nil {
                fmt.Println("failed:", noteID, err)
                return
            }
            mu.Lock()
            out[noteID] = body
            mu.Unlock()
        }(id)
    }
    wg.Wait()
    return out
}

Three things: match concurrency to the key's rate limit (start at 4 if unsure); the defer func() { <-sem }() is mandatory — without it a panic leaks the slot forever and the program deadlocks; and lock around the shared map, because Go maps are not concurrency-safe and skipping it gives you a hard fatal error: concurrent map writes.

Common errors

Status Meaning What to do
401 Key missing or invalid Check you're setting req.Header.Set("X-API-Key", key), not Authorization
402 Insufficient balance Top up. Retrying does nothing — the request never ran and nothing was charged
429 Rate limited Lower concurrency and back off; see above
422 Invalid parameters The body names the field. Remember crawler endpoints are GET + query, not a JSON body

There's one Go-specific trap: you must close resp.Body. Miss defer resp.Body.Close() and connections never return to the pool — a few thousand requests in, you start seeing timeouts that make no sense.

Set timeouts, and not just one

client := &http.Client{Timeout: 30 * time.Second}

The zero value of http.Client has no timeout at all — one stuck connection can hang the entire job forever. Batch jobs hit this almost every time. For finer control, use context.WithTimeout per request so retry logic can abandon a hopeless call sooner.

Next steps

Sign up free and send your first request with Go now.