# Pinterest Scraper - Pins, Boards, Images, Links (`s-r/pinterest-scraper`) Actor

Search Pinterest and get structured pins: title, description, the full-size image, the link and domain behind the pin, the board it sits on and who pinned it. Walks pages with Pinterest's own bookmark pagination.

- **URL**: https://apify.com/s-r/pinterest-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** Social media, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Pinterest Scraper

Search Pinterest and get pins back as structured rows: the title, the
description, the full-size image, the link and domain behind the pin, the board
it sits on and who pinned it.

No login, no cookies, no browser.

### What each pin gives you

- `title`, `description` and `alt_text`. The alt text is worth knowing about:
  Pinterest auto-generates it, so it is often filled in on pins whose title is
  empty, which makes it a useful fallback rather than a duplicate.
- `image_url`, plus `image_width` and `image_height`. This is the **largest
  published rendition**, not the thumbnail. Pinterest keys its renditions by
  width, so taking the first one you find gives you whichever happened to
  serialise first, which is usually a 236-pixel preview.
- `link` and `domain` — where the pin points. See the note below on what
  `domain` deliberately does not contain.
- `board_name`, `board_url`, `pinner_username`, `pinner_full_name` and
  `pinner_followers`
- `reactions`, summed across all reaction types
- `is_promoted`, so paid placements can be filtered out of any ranking
- `dominant_color`, which Pinterest computes for each image
- `created_at` and a direct `url` to the pin

### Two details that keep the output honest

**"Uploaded by user" is not a domain.** Pinterest writes that phrase into the
domain field for pins uploaded straight to the platform rather than saved from
a website. Storing it as-is means every count grouped by domain has a large
fake entry at the top that looks like a real site. Those pins are reported with
an empty domain instead, and the summary says how many carried a real one.

**Save counts are not reported, because this endpoint does not carry them.**
The structure that would hold them is present on every pin and empty on every
pin, measured across a full page. A column that is null on every row is worse
than an absent one: it reads as "this pin has no saves" rather than "saves are
not available here". Reaction counts, which are populated, are reported
instead.

### Paging

Pinterest hands back an opaque `bookmark` string with each response and expects
it replayed verbatim on the next call. It signals the end of the results with a
`-end-` sentinel rather than by omitting the field, so a scraper that treats
any bookmark as "more data" re-requests the same last page until its budget
runs out. This stops on the sentinel.

Expect roughly 18 to 25 pins per page.

### Input

- **Search terms** — `kitchen ideas`, `minimalist desk setup`
- **What to search** — pins, boards or users. Pins is the usual choice.
- **Pages per term**, **maximum pins per term**, **maximum search terms**
- **Terms in parallel** and **retries**
- **Request from country**, optionally

### Run summary

Pins returned, how many carried a title, a source link and a real domain, how
many were promoted, and the number of distinct boards and pinners.

Those distinct counts are the useful health check on a run: a large pin count
spread across very few boards usually means one prolific account rather than
broad coverage of the topic.

### What people use this for

**Visual trend research.** The pins surfaced for a term are what Pinterest
currently considers relevant to it, so running the same terms over time is a
readable signal of what a category looks like right now.

**Traffic-source discovery.** `domain` shows which sites are actually earning
saves in a niche. Because pins uploaded natively are excluded from that field
rather than lumped under a fake label, the resulting domain counts mean
something.

**Content and SEO research.** `title`, `description` and `alt_text` together
are a corpus of the language people use for a topic, written by people trying
to be found for it.

**Creator identification.** `pinner_username` and `pinner_followers` across a
set of terms produce a ranked list of accounts active in a niche.

**Image sourcing.** `image_url` is the full-resolution rendition, with width
and height, so a downstream pipeline can filter by size before downloading
anything.

### Scale and cost

Each page is one request and returns roughly 18 to 25 pins, so a term at three
pages costs three requests. Two terms at three pages each returned 135 pins in
12 seconds in testing, and a single term at two pages returned 43 pins in six.

There is no per-pin follow-up request, so the cost of a run is exactly the
number of pages you asked for. Raising **pages per term** is the only thing
that makes a run longer.

### Notes

Everything is read at the moment of the fetch. Pinterest re-ranks results
constantly, so two runs of the same term hours apart will legitimately return
overlapping but different sets. `pin_id` is stable, so deduplicating across
runs is straightforward.

Rows are deduplicated on `pin_id` within a run, so overlapping search terms
give you the union rather than repeats.

Boards and users scope return the same shape with the pin-specific fields
empty, since those records are boards and profiles rather than pins.

# Actor input Schema

## `queries` (type: `array`):

What to search Pinterest for, for example 'kitchen ideas' or 'minimalist desk setup'.

## `scope` (type: `string`):

Pins is the usual choice. Boards and users search the same way but return board and profile records instead of pins.

## `pages` (type: `integer`):

How many pages to walk for each term. Pinterest returns roughly 18 to 25 pins per page and hands back a bookmark for the next one.

## `pins_per_query` (type: `integer`):

Upper bound on rows returned for each search term.

## `max_queries` (type: `integer`):

How many terms to process in one run.

## `concurrency` (type: `integer`):

How many search terms to fetch at the same time.

## `retries` (type: `integer`):

Retries with backoff before a page is reported as an error.

## `country` (type: `string`):

Optional two-letter country code to request from, for example us. Leave empty to request from wherever the run happens to sit.

## Actor input object example

```json
{
  "queries": [
    "kitchen ideas",
    "minimalist desk"
  ],
  "scope": "pins",
  "pages": 3,
  "pins_per_query": 250,
  "max_queries": 30,
  "concurrency": 4,
  "retries": 3,
  "country": "us"
}
```

# Actor output Schema

## `pins` (type: `string`):

One row per pin.

## `summary` (type: `string`):

Counts, unique boards and pinners, and how many pins carried a source link.

## `errors` (type: `string`):

Searches that could not be read, with a code and a redacted message.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "queries": [
        "kitchen ideas",
        "minimalist desk"
    ],
    "pages": 3,
    "pins_per_query": 250,
    "max_queries": 30,
    "concurrency": 4,
    "retries": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/pinterest-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "queries": [
        "kitchen ideas",
        "minimalist desk",
    ],
    "pages": 3,
    "pins_per_query": 250,
    "max_queries": 30,
    "concurrency": 4,
    "retries": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/pinterest-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "kitchen ideas",
    "minimalist desk"
  ],
  "pages": 3,
  "pins_per_query": 250,
  "max_queries": 30,
  "concurrency": 4,
  "retries": 3
}' |
apify call s-r/pinterest-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/pinterest-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/JuorigpWt8dkFGPz7/builds/Lf46jbpuOR8TO7Hk4/openapi.json
