Pinterest Scraper — Search, Boards & Profiles avatar

Pinterest Scraper — Search, Boards & Profiles

Pricing

from $1.99 / 1,000 pin scrapeds

Go to Apify Store
Pinterest Scraper — Search, Boards & Profiles

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

subimpact

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

7 days ago

Last modified

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

FieldTypeDefaultNotes
modesearch | board | profilesearchWhich surface to scrape
queriesstringComma-separated keywords (mode=search)
boardUrlsstringComma-separated board URLs or /user/board/ paths (mode=board)
boardIdsstringComma-separated numeric board ids (mode=board, skips URL resolution)
usernamesstringComma-separated usernames (mode=profile)
maxPinsPerSourceint500Cost ceiling per source. Hard cap 5000
imageSizeorig | 736x | 474xorigWhich variant lands in imageUrl
downloadMediaboolfalseStore the file in the key-value store, billed per file
enrichDetailsboolfalseOne extra request per pin for extra fields
includeRawbooltrueKeep the untouched pin object in raw
proxyConfigurationobjectoffNot 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):

RequestResult
User-Agent only403
User-Agent + x-pinterest-pws-handler200
Everything except x-pinterest-pws-handler403
Everything, with a deliberately bogus x-app-version200
Everything except x-app-version200
Everything except Cookie200
Everything except X-Requested-With200

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

ModeResourcedata shape
search pinsBaseSearchResource (scope: "pins")dict → data.results[]
search boardsBaseSearchResource (scope: "boards")dict → data.results[]
board pinsBoardFeedResourcelistdata[]
profile pinsUserPinsResourcelistdata[]
pin detailPinResourcedict → 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:

  • Pidgetshttps://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/.
  • RSShttps://www.pinterest.com/{user}/feed.rss returns 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 Request
  • POST /_/graphql/ → needs a live browser session
  • Board RSS (/{user}/{board}/feed.rss) and any .rss suffix → 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 boardUrls resolution works)

Pay-per-event

EventWhen
apify-actor-startOnce at start
pin-scrapedPer pin pushed, charged incrementally per batch
media-downloadedPer file stored, only when downloadMedia is on

Every charge is wrapped in try/catch — a failed charge warns and the run continues.

Development

npm install
npm 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.