How to Scrape Pinterest Pins and Boards in 2026: A Developer's Guide

Learn how to scrape Pinterest pins, boards, and search results using the internal Resource API and rotating residential proxies. Includes Python and Node.js examples, pagination patterns, and anti-bot mitigation strategies.

How to Scrape Pinterest Pins and Boards in 2026: A Developer's Guide

Legal caveat: This guide covers access to public Pinterest data only. Scraping may violate Pinterest's Terms of Service, and unauthorized access to protected systems can implicate the U.S. Computer Fraud and Abuse Act (CFAA). In the EU, collecting personal data may trigger GDPR obligations. Always review Pinterest's Terms of Service and applicable laws before scraping, and prefer the official Pinterest API v5 for production workloads.

Building a visual-content or trend dataset often means you need to scrape Pinterest pins, board feeds, and search results. Pinterest is one of the largest visual discovery platforms, with over 518 million monthly active users as of Q1 2025, and its boards are a goldmine of structured trend data — product images, descriptions, links, and category tags. But Pinterest's anti-bot stack has matured significantly, and naive scraping fails quickly. This guide walks through how to scrape Pinterest pins and boards in 2026 using the platform's internal Resource API, rotating residential proxies, and proper request hygiene.

How to Scrape Pinterest Pins and Boards in 2026: The Landscape

Before diving into code, you need to understand what data is accessible and what isn't. Pinterest has two distinct data surfaces:

Public surfaces (no login required)

  • Pin pages: Individual pin URLs like pinterest.com/pin/123456789/ render publicly with image, title, description, and outbound link.
  • Board feeds: Public boards display their pins to anonymous visitors.
  • Search results: Keyword searches return pins and boards without authentication.
  • User profiles: Public profiles show boards and pin counts.

Login-walled surfaces

  • Home feed: The personalized recommendation feed requires authentication.
  • Following feed: Content from accounts a user follows.
  • Private boards: Secret boards are not accessible.

Official Pinterest API v5

Pinterest offers a REST API (v5) with endpoints for pins, boards, and analytics. It requires OAuth 2.0 and an approved developer app. Rate limits are documented at roughly 1,000 requests per 2-minute window for write operations and more generous limits for reads, but access to full pin metadata — especially for boards you don't own — is limited. For large-scale trend analysis or competitive research, the API alone often isn't sufficient, which is why teams turn to Pinterest API scraping via the internal Resource endpoints.

Understanding Pinterest's Internal Resource API

Pinterest's web frontend is a React single-page application that communicates with backend services through a set of internal endpoints collectively called the Resource API. These endpoints return JSON and are the backbone of every page you see. Key resources include:

  • /resource/PinResource/get/ — fetches a single pin's full metadata.
  • /resource/BoardFeedResource/get/ — paginated list of pins on a board.
  • /resource/SearchResource/get/ — search results for a keyword query.
  • /resource/BoardResource/get/ — board metadata (title, description, follower count).
  • /resource/UserResource/get/ — user profile data.

Each request is a GET (or sometimes POST) to https://www.pinterest.com/resource/{ResourceName}/get/ with two critical query parameters:

  • source_url= — the human-readable URL path that corresponds to the page (e.g., /search/pins/?q=home%20decor).
  • data= — a URL-encoded JSON object containing the resource-specific parameters (pagination cursor, query term, board ID, etc.).

Required headers

Pinterest's backend inspects several headers to validate requests. Missing or mismatched values trigger 403 responses or silent HTML redirects:

HeaderPurposeExample
X-Pinterest-PWS-HandlerIdentifies the frontend handler that generated the requestboards/[username]/[slug].js
X-APP-VERSIONFrontend build version; must match a current deploy9a8b7c6
csrftokenCSRF token from the initial page load cookiesabc123def456
User-AgentMust be a realistic, current browser UAChrome 130 on macOS
AcceptJSON content type expectedapplication/json

The X-APP-VERSION value changes with each frontend deploy. You can extract it from the initial HTML page (look for __PWS_INITIAL_DATA__ or script tags referencing versioned chunks). If your version is stale by more than a few days, requests start failing.

Anti-Bot Reality: Why You Need Rotating Residential Proxies

Pinterest employs per-IP rate limiting and behavioral bot scoring. Based on community testing and observable behavior, thresholds are roughly:

  • ~50–80 requests per IP per minute before soft throttling (HTTP 429 or empty responses).
  • ~150–200 requests per IP in a short burst before a temporary IP block (403 for 1–24 hours).
  • Repeated blocks can lead to longer bans or permanent IP blacklisting.

Additionally, Pinterest localizes search results and recommendations. A search for "wedding centerpieces" from a US IP returns different pins than the same search from a German IP. If your dataset needs consistent, geo-specific results, you must pin your requests to a target country.

This is why a Pinterest scraper at any meaningful scale requires rotating residential proxies with geo-targeting. Datacenter IPs are flagged almost immediately by Pinterest's edge. Residential IPs blend with organic user traffic, and rotation distributes your request volume across many source IPs to stay under per-IP limits.

ProxyHat Setup for Pinterest Scraping

ProxyHat provides residential proxies with country and city-level geo-targeting and sticky sessions. For Pinterest, you'll want:

  • Rotating residential IPs with -country-US (or your target market) for localized search results.
  • Sticky sessions (-session-) when you need csrftoken continuity across a pagination sequence.
  • Pacing — 1–3 second delays between requests per session to mimic human browsing.

Connection details:

# HTTP proxy (default)
http://USERNAME:PASSWORD@gate.proxyhat.com:8080

# With US geo-targeting
http://user-country-US:pass@gate.proxyhat.com:8080

# With sticky session for token continuity
http://user-country-US-session-abc123:pass@gate.proxyhat.com:8080

# SOCKS5 proxy
socks5://USERNAME:PASSWORD@gate.proxyhat.com:1080

You can explore ProxyHat's pricing and proxy locations to plan your setup. For broader context on proxy-based scraping, see our web scraping use case page.

Python Example: Scraping a Board Feed with Pagination

This example fetches pins from a public board using BoardFeedResource, paginating via the bookmark cursor. It uses ProxyHat residential proxies with a sticky session to maintain csrftoken continuity across the pagination sequence.

import requests
import json
import time
import urllib.parse

PROXY = "http://user-country-US-session-board1:pass@gate.proxyhat.com:8080"
PROXIES = {"http": PROXY, "https": PROXY}

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/130.0.0.0 Safari/537.36",
    "Accept": "application/json, text/javascript, */*; q=0.01",
    "Accept-Language": "en-US,en;q=0.9",
    "X-Pinterest-PWS-Handler": "boards/[username]/[slug].js",
    "X-APP-VERSION": "9a8b7c6",  # extract from initial page load
    "Referer": "https://www.pinterest.com/",
}

BASE = "https://www.pinterest.com/resource/BoardFeedResource/get/"

# Example: board "username/spring-decor"
BOARD_ID = "123456789012345678"
SOURCE_URL = urllib.parse.quote("/username/spring-decor/", safe="")

def fetch_board_pins(board_id, max_pages=10):
    pins = []
    bookmark = None

    # First request: get csrftoken from cookies
    session = requests.Session()
    session.proxies = PROXIES

    # Prime the session with an initial page load
    init_resp = session.get(
        "https://www.pinterest.com/",
        headers=HEADERS
    )
    csrf = session.cookies.get("csrftoken", "")
    HEADERS["csrftoken"] = csrf
    time.sleep(2)

    for page in range(max_pages):
        data_obj = {
            "options": {
                "board_id": board_id,
                "page_size": 25,
                "currentFilter": -1,
                "field_set_key": "react_grid_pin",
            },
            "context": {}
        }
        if bookmark:
            data_obj["options"]["bookmarks"] = [bookmark]

        data_param = urllib.parse.quote(json.dumps(data_obj), safe="")
        params = {
            "source_url": SOURCE_URL,
            "data": data_param,
        }

        resp = session.get(BASE, headers=HEADERS, params=params, timeout=30)
        if resp.status_code != 200:
            print(f"Page {page}: HTTP {resp.status_code}, stopping.")
            break

        payload = resp.json()
        resource_response = payload.get("resource_response", {})
        page_pins = resource_response.get("data", [])

        if not page_pins:
            print(f"Page {page}: no pins returned, end of board.")
            break

        for pin in page_pins:
            pins.append({
                "id": pin.get("id"),
                "title": pin.get("title", ""),
                "description": pin.get("description", ""),
                "image_url": (
                    pin.get("images", {})
                       .get("orig", {})
                       .get("url", "")
                ),
                "link": pin.get("link", ""),
                "board_id": pin.get("board", {}).get("id", ""),
            })

        # Extract bookmark for next page
        bookmark_list = resource_response.get("bookmark")
        if bookmark_list and len(bookmark_list) > 0:
            bookmark = bookmark_list[0]
        else:
            break

        print(f"Page {page}: fetched {len(page_pins)} pins (total: {len(pins)})")
        time.sleep(2)  # pacing between pages

    return pins

pins = fetch_board_pins(BOARD_ID, max_pages=10)
print(f"\nTotal pins scraped: {len(pins)}")
if pins:
    print(json.dumps(pins[0], indent=2))

A single pin object from BoardFeedResource is large (often 2–5 KB of JSON). The truncated extraction above pulls the most useful fields. The full object includes rich metadata: close-up images, dominant color, repin count, video URLs (for video pins), and category.

Node.js Example: Scraping Pinterest Search Results

This Node.js example queries SearchResource for a keyword, using the ProxyHat HTTP gateway on port 8080 with US geo-targeting.

const axios = require('axios');
const querystring = require('querystring');

const PROXY_URL = 'http://user-country-US-session-search1:pass@gate.proxyhat.com:8080';

const HEADERS = {
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' +
    'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
  'Accept': 'application/json, text/javascript, */*; q=0.01',
  'Accept-Language': 'en-US,en;q=0.9',
  'X-Pinterest-PWS-Handler': 'search/[scope].js',
  'X-APP-VERSION': '9a8b7c6',
  'Referer': 'https://www.pinterest.com/',
};

const BASE = 'https://www.pinterest.com/resource/SearchResource/get/';

async function searchPins(keyword, maxPages = 5) {
  const allPins = [];
  let bookmark = null;

  const client = axios.create({
    proxy: false,
    httpsAgent: new (require('https-proxy-agent').HttpsProxyAgent)(PROXY_URL),
    timeout: 30000,
  });

  // Prime session for csrftoken
  const initResp = await client.get('https://www.pinterest.com/', { headers: HEADERS });
  const cookies = initResp.headers['set-cookie'] || [];
  const csrfMatch = cookies.find(c => c.startsWith('csrftoken='));
  const csrf = csrfMatch ? csrfMatch.split('=')[1].split(';')[0] : '';
  HEADERS['csrftoken'] = csrf;

  await new Promise(r => setTimeout(r, 2000));

  for (let page = 0; page < maxPages; page++) {
    const options = {
      query: keyword,
      scope: 'pins',
      page_size: 25,
    };
    if (bookmark) options.bookmarks = [bookmark];

    const dataParam = encodeURIComponent(JSON.stringify({ options, context: {} }));
    const sourceUrl = encodeURIComponent(`/search/pins/?q=${encodeURIComponent(keyword)}`);

    const url = `${BASE}?source_url=${sourceUrl}&data=${dataParam}`;

    try {
      const resp = await client.get(url, { headers: HEADERS });
      if (resp.status !== 200) {
        console.log(`Page ${page}: HTTP ${resp.status}, stopping.`);
        break;
      }

      const resourceResp = resp.data.resource_response || {};
      const results = resourceResp.data || [];
      if (!results.length) {
        console.log(`Page ${page}: no results, end of search.`);
        break;
      }

      for (const pin of results) {
        allPins.push({
          id: pin.id,
          title: pin.title || '',
          description: pin.description || '',
          image_url: pin.images?.orig?.url || '',
          link: pin.link || '',
        });
      }

      const bookmarks = resourceResp.bookmark;
      if (bookmarks && bookmarks.length) {
        bookmark = bookmarks[0];
      } else {
        break;
      }

      console.log(`Page ${page}: ${results.length} pins (total: ${allPins.length})`);
      await new Promise(r => setTimeout(r, 2000));
    } catch (err) {
      console.error(`Page ${page} error:`, err.message);
      break;
    }
  }

  return allPins;
}

searchPins('minimalist home office', 5)
  .then(pins => {
    console.log(`\nTotal pins: ${pins.length}`);
    if (pins.length) console.log(JSON.stringify(pins[0], null, 2));
  })
  .catch(console.error);

Pagination, Sticky Sessions, and Fingerprint Hygiene

Bookmark/cursor pagination

Pinterest uses opaque bookmark strings as pagination cursors. The pattern is:

  1. First request: no bookmark in the data param.
  2. Response includes a bookmark array (usually one element).
  3. Pass that bookmark in options.bookmarks on the next request.
  4. When the response returns an empty data array or a -end- bookmark, you've reached the end.

Bookmarks are session-specific. If you change your IP mid-sequence, the bookmark may become invalid. This is where sticky sessions matter.

Sticky sessions for csrftoken continuity

Use ProxyHat's -session- flag to keep the same exit IP for a pagination sequence. This ensures your csrftoken and IP remain consistent, which is what Pinterest expects from a real browser session:

# Sticky session for one board scrape sequence
http://user-country-US-session-board-abc123:pass@gate.proxyhat.com:8080

Rotate the session ID (e.g., board-abc123board-def456) between boards or search queries to get a fresh IP while maintaining continuity within a single logical operation.

Pacing

Human users don't fire 25 requests per second. Aim for 1–3 seconds between paginated requests and 5–10 seconds between different boards or searches. If you're running concurrent workers, keep total aggregate throughput under ~50 requests per minute per IP.

User-Agent and header hygiene

  • Use a current, real browser User-Agent string. Check MDN's UA reference for format guidance.
  • Keep Accept-Language consistent with your geo-target (e.g., en-US,en;q=0.9 for US proxies).
  • Include a realistic Referer — for board feeds, use the board URL; for search, use the search URL.
  • Update X-APP-VERSION regularly. Stale versions are a strong bot signal.
  • Don't send TLS fingerprints that mismatch your claimed UA. If using Python requests, consider curl_cffi or tls-client to match Chrome's TLS fingerprint.

Common Mistakes and Edge Cases

  • Stale X-APP-VERSION: The most common failure. Pinterest deploys multiple times per week. Extract the version fresh before each scraping run.
  • Missing csrftoken header: The token must match the cookie. If you rotate IPs mid-session without a sticky proxy, the token becomes invalid.
  • Over-paginating: Some boards have 10,000+ pins. Set a max_pages cap to avoid runaway requests and respect data minimization principles.
  • Ignoring robots.txt: Check Pinterest's robots.txt before scraping. It specifies crawl-delay and disallowed paths.
  • Scraping personal data: Pin descriptions, comments, and user profile data may constitute personal data under GDPR. Stick to public, non-personal pin metadata (images, titles, links, board structure).
  • Not handling 429s: Implement exponential backoff. A single 429 doesn't mean you're banned — it means you hit the rate limit. Back off for 30–60 seconds and retry with a new session.

Comparison: Resource API vs Official API v5 vs HTML Scraping

ApproachData DepthRate LimitsAuth RequiredReliability
Internal Resource APIFull pin metadata, images, linksPer-IP, ~50–80 req/minAnonymous (csrftoken from page load)Medium — breaks on frontend deploys
Official API v5Limited to owned/authorized boardsDocumented, stableOAuth 2.0 appHigh — versioned, supported
HTML scrapingWhat's visible in rendered HTML onlySame per-IP limitsNoneLow — breaks on any DOM change

Ethical Scraping: When to Use the Official API Instead

Scraping Pinterest's internal Resource API exists in a gray zone. It's technically public data served to anonymous browsers, but automated access at scale may violate Pinterest's ToS. Here are guidelines for responsible practice:

  1. Collect only public, non-personal data. Pin images, titles, descriptions, links, and board structure are generally fair game. Avoid scraping user profiles, comments, or any data tied to identifiable individuals.
  2. Respect robots.txt. Pinterest's robots.txt defines what crawlers are allowed to access. Honor crawl-delay directives.
  3. Minimize data collection. Collect only what your use case requires. Don't archive full pin objects if you only need titles and image URLs.
  4. Use the official API for production. If you're building a commercial product, apply for Pinterest API v5 access. It's stable, documented, and won't break when the frontend deploys.
  5. Consider GDPR/CCPA implications. If pin descriptions contain personal data (names, emails, phone numbers), you may be processing personal data under EU/California law. Implement data retention limits and deletion procedures.
  6. Rate-limit yourself. Don't degrade Pinterest's service. Keep your request volume reasonable and back off on errors.

For SERP tracking and broader web-scraping workflows, see our SERP tracking use case and the ProxyHat documentation for proxy configuration details.

Key Takeaways

  • Pinterest's internal Resource API (BoardFeedResource, SearchResource, PinResource) returns rich JSON for public pins, boards, and search results without authentication — but requires correct headers (X-APP-VERSION, csrftoken, X-Pinterest-PWS-Handler).
  • Per-IP rate limits (~50–80 req/min) and bot scoring make rotating residential proxies with geo-targeting essential. Use -country-US for localized results and -session- for pagination continuity.
  • Pagination uses opaque bookmark cursors. Keep the same IP (sticky session) for an entire pagination sequence to avoid invalidating bookmarks.
  • Pace requests at 1–3 seconds between pages. Update X-APP-VERSION regularly. Match your TLS fingerprint to your User-Agent.
  • For production systems, prefer the official Pinterest API v5. Use Resource API scraping for research, trend analysis, and datasets where the official API's data scope is insufficient.
  • Always respect Pinterest's ToS, robots.txt, and applicable laws (CFAA, GDPR, CCPA). Collect only public, non-personal data.

FAQ

What is Pinterest Resource API scraping?

Pinterest Resource API scraping refers to calling Pinterest's internal JSON endpoints (like /resource/BoardFeedResource/get/ or /resource/SearchResource/get/) that the web frontend uses to load data. These endpoints return structured JSON for public pins, boards, and search results. You construct requests with a source_url parameter and a URL-encoded data JSON object, along with headers like X-APP-VERSION and csrftoken. It's more reliable than HTML scraping but requires maintaining correct headers and handling pagination via bookmark cursors.

Why do you need proxies to scrape Pinterest?

Pinterest enforces per-IP rate limits of roughly 50–80 requests per minute and uses bot scoring to detect automated traffic. Without proxies, a single IP gets throttled or blocked after a few hundred requests. Rotating residential proxies distribute your request volume across many source IPs, each appearing as organic user traffic. Geo-targeting (e.g., -country-US) is also important because Pinterest localizes search results and recommendations by region, so your data quality depends on the IP's geographic origin.

Which proxy type works best for scraping Pinterest?

Residential proxies are the best choice for Pinterest scraping. Datacenter IPs are quickly flagged by Pinterest's anti-bot systems. Residential IPs come from real ISPs and blend with organic traffic. Use rotating residential proxies with country-level geo-targeting for search scraping, and sticky sessions (-session-) for paginated board feeds where you need csrftoken and bookmark continuity. Mobile proxies also work well but are typically more expensive and better suited for high-trust scenarios.

How do you avoid blocks when scraping Pinterest?

To avoid blocks: (1) use rotating residential proxies with geo-targeting via ProxyHat (gate.proxyhat.com:8080); (2) pace requests at 1–3 seconds between paginated calls and 5–10 seconds between different boards or searches; (3) maintain header hygiene — current User-Agent, matching Accept-Language to your geo, fresh X-APP-VERSION; (4) use sticky sessions for pagination sequences to keep csrftoken valid; (5) implement exponential backoff on HTTP 429 responses; (6) keep aggregate throughput under ~50 requests per minute per IP.

Is scraping Pinterest legal?

Scraping public data from Pinterest may be permissible in some jurisdictions but can violate Pinterest's Terms of Service. In the US, the CFAA has been narrowed by the Van Buren ruling, but unauthorized access to protected systems remains risky. In the EU, collecting personal data triggers GDPR obligations. Always review Pinterest's ToS and robots.txt, collect only public non-personal data, and prefer the official Pinterest API v5 for production workloads. This guide is for educational purposes and does not constitute legal advice.

Ready to get started?

Access 50M+ residential IPs across 148+ countries with AI-powered filtering.

View PricingResidential Proxies
← Back to Blog