RedNote (Xiaohongshu) API Quickstart with 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
- Bulk content: keyword search, watermark-free image/video
- Users & products: comment data, product data
- Full endpoints: API docs
Sign up free and send your first request with Go now.