Pinterest Scraper — Search, Boards & Profiles
Pricing
from $1.99 / 1,000 pin scrapeds
Pinterest Scraper — Search, Boards & Profiles
Scrape Pinterest pins from keyword search, boards and profiles. Full-res originals, video variants, board/pinner metadata, repin counts. Pure HTTP - no browser, no login, no API key, no proxy.
Pricing
from $1.99 / 1,000 pin scrapeds
Rating
0.0
(0)
Developer
subimpact
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
7 days ago
Last modified
Categories
Share
Pinterest Scraper
Scrape Pinterest pins from keyword searches, any public board, or any public profile.
Pure HTTP against Pinterest's own /resource/ API — no browser, no login, no API key, no proxy
required. A single GET https://www.pinterest.com/ mints the whole session.
Input
| Field | Type | Default | Notes |
|---|---|---|---|
mode | search | board | profile | search | Which surface to scrape |
queries | string | — | Comma-separated keywords (mode=search) |
boardUrls | string | — | Comma-separated board URLs or /user/board/ paths (mode=board) |
boardIds | string | — | Comma-separated numeric board ids (mode=board, skips URL resolution) |
usernames | string | — | Comma-separated usernames (mode=profile) |
maxPinsPerSource | int | 500 | Cost ceiling per source. Hard cap 5000 |
imageSize | orig | 736x | 474x | orig | Which variant lands in imageUrl |
downloadMedia | bool | false | Store the file in the key-value store, billed per file |
enrichDetails | bool | false | One extra request per pin for extra fields |
includeRaw | bool | true | Keep the untouched pin object in raw |
proxyConfiguration | object | off | Not needed — kept as an escape hatch |
boardUrls are resolved to a numeric board_id automatically by reading the board page.
boardIds skips that request, so use it if you already have the id.
Read this before using search mode
Pinterest's search payload contains images only. A search result carries exactly seven fields:
id, node_id, type, images, image_signature, dominant_color, tracking_params
No title, no description, no board, no pinner, no repin_count. Those columns come back
null in search mode. Board and profile payloads are rich.
If you need text and attribution from search results, enable enrichDetails — but see the
caveat under Pin detail below. The Actor logs a warning when you run search mode without it.
Output
One row per pin, in this column order:
runId, mode, source, page, pinId, pinUrl, title, description, link, domain,imageSignature, imageOrigUrl, image736Url, image474Url, isVideo, videoUrls,videoStatus, isRepin, repinCount, commentCount, boardId, boardName, boardUrl,boardPinCount, boardFollowerCount, pinnerId, pinnerUsername, pinnerFullName,pinnerFollowerCount, dominantColor, altText, isPromoted, priceValue, createdAt,scrapedAt, image, imageUrl, videoUrl, mediaKey, mediaBytes, raw
Fields that are always null because Pinterest omits them from feed payloads:
boardPinCount, boardFollowerCount, pinnerFollowerCount. They are kept in the schema because
board search results do carry pin_count.
title and description are blank on the large majority of real pins — Pinterest simply does not
require them. altText is usually the most descriptive text available: it falls back to
Pinterest's auto-generated caption (auto_alt_text), e.g. "an empty room with a radiator on the
wall".
How it works
Session
GET https://www.pinterest.com/ with a browser User-Agent sets csrftoken and _pinterest_sess,
and its SSR HTML embeds the build token at "appVersion":"…" inside <script id="__PWS_DATA__">.
Both are read at runtime, never hardcoded.
Every /resource/ JSON response also echoes the live token at client_context.app_version, so the
client refreshes its own token from ordinary traffic for free.
The 403 headers
Requests to /resource/{Name}/get/ need the right headers or Pinterest answers
403 Invalid Resource Request. Header ablation against the live endpoint (2026-09-08):
| Request | Result |
|---|---|
| User-Agent only | 403 |
User-Agent + x-pinterest-pws-handler | 200 |
Everything except x-pinterest-pws-handler | 403 |
Everything, with a deliberately bogus x-app-version | 200 |
Everything except x-app-version | 200 |
Everything except Cookie | 200 |
Everything except X-Requested-With | 200 |
x-pinterest-pws-handler: www/search/[scope].js is the single load-bearing header.
x-app-version is not validated today. The Actor still reads it at runtime and sends the full
browser-like header set, because it costs nothing and any of these can start being enforced.
Endpoints
| Mode | Resource | data shape |
|---|---|---|
| search pins | BaseSearchResource (scope: "pins") | dict → data.results[] |
| search boards | BaseSearchResource (scope: "boards") | dict → data.results[] |
| board pins | BoardFeedResource | list → data[] |
| profile pins | UserPinsResource | list → data[] |
| pin detail | PinResource | dict → single object |
That list-vs-dict split is the main parsing trap; unwrapResource() normalizes all of them.
Pagination
Page 2+ is the same options object plus bookmarks: [<previous bookmark>]. Paging stops when
the bookmark is missing, is -end-, repeats itself, or a page yields no new pin ids.
Correctness is measured with a distinct-id rule, not a sum of page sizes: Pinterest's page sizes drift (24 / 25 / 26 per page) and a bookmark may legitimately re-serve an item.
Story modules
Board and search feeds interleave type: "story" items — "Related interests" rails, not pins. They
have no images, and their title is an object, so String(item.title) would write
"[object Object]" into your dataset. They are filtered out. fixtures/board-small.json is 7 raw
items = 5 pins + 2 story modules.
Video pins
is_video is absent from search payloads even on actual video pins, so detection uses
videos.video_list. That map holds V_HLSV4 / V_HLSV3_MOBILE (.m3u8 playlists, not
downloadable files) and V_720P (a real .mp4). videoUrls is sorted MP4-first, and
downloadMedia never tries to download a playlist.
Images
images.orig.url is the full-resolution original and is directly fetchable.
Never build a variant URL by swapping the size segment of another one. On ~11% of pins
(14 of 127 in the fixtures) the original is stored under a different extension than its
derivatives — orig is .png while 736x/474x are .jpg. Always read the URL out of the pin's
images map. When a download fails the Actor falls back through the other variants it was given.
A Referer: https://www.pinterest.com/ header is sent to i.pinimg.com because that is what a
browser does, but it is not currently required — referer / UA-only / no-headers all returned
identical statuses on 2026-09-08.
Pin detail (enrichDetails)
Unauthenticated PinResource returns a reduced field set — 27 keys with no id, title,
description or images. What you get is seo_title, seo_description, image_medium_url,
pin_join annotations and closeup_unified_attribution. Enrichment fills gaps only; it never
overwrites good feed data, and the pin id is recovered by base64-decoding node_id (Pin:<id>).
In practice it does recover real text — a search pin with a null title came back as
"Cybersecurity aesthetic wallpaper | Cybersecurity wallpaper iphone, …".
Rate limiting
Requests are paced to ~3/s. Retries use exponential backoff on 403/429/5xx; a 403 also refreshes the session before retrying. No 429 was observed at this rate.
Fallback surfaces
Documented, not used by the main pump — both cap out and cannot paginate:
- Pidgets —
https://api.pinterest.com/v3/pidgets/users/{user}/pins/returns clean JSON (no JSONP padding to strip). Caps at 50 pins. Board variant:pidgets/boards/{user}/{board}/pins/. - RSS —
https://www.pinterest.com/{user}/feed.rssreturns real XML, 23 items, no pagination. Note its<title>elements are blank, same as the API.
Parsers for both ship in src/parse.js (parsePidgets, parseRssItems).
Dead surfaces — do not use
PinSearchResource,SearchResource,/_ngjs/resource/*→403 Invalid Resource RequestPOST /_/graphql/→ needs a live browser session- Board RSS (
/{user}/{board}/feed.rss) and any.rsssuffix → SPA HTML, not XML - Profile/board SSR pages → the pin grid is client-rendered; no pin data in the HTML
(the board page does still contain the board id, which is how
boardUrlsresolution works)
Pay-per-event
| Event | When |
|---|---|
apify-actor-start | Once at start |
pin-scraped | Per pin pushed, charged incrementally per batch |
media-downloaded | Per file stored, only when downloadMedia is on |
Every charge is wrapped in try/catch — a failed charge warns and the run continues.
Development
npm installnpm test # node --test, 90 tests, fully offline
Tests run against fixtures/, captured live on 2026-09-08. They never touch the network, and the
assertions are byte-exact so a failure means the code changed, not the site.