Pinterest Ads Scraper avatar
Pinterest Ads Scraper

Pricing

$35.00/month + usage

Go to Store
Pinterest Ads Scraper

Pinterest Ads Scraper

Developed by

Lexis Solutions

Lexis Solutions

Maintained by Community

Scrape Pinterest ads to extract trending product insights, campaign performance, and audience data. Ideal for market research, competitor tracking, and digital marketing optimization. Fast, structured, and customizable ad data.

5.0 (3)

Pricing

$35.00/month + usage

14

Total users

10

Monthly users

4

Runs succeeded

>99%

Last modified

2 days ago

.gitignore

# This file tells Git which files shouldn't be added to source control
.idea
dist
node_modules
storage
# Added by Apify CLI
.venv

Dockerfile

FROM apify/actor-node-puppeteer-chrome:20
USER root
# install tsx
RUN yarn global add tsx
USER myuser
COPY package.json yarn.lock ./
# install dependencies
RUN yarn --production
# copy the source code
COPY . ./
CMD yarn start

package.json

{
"name": "pinterest-ads-scraper",
"version": "0.0.1",
"type": "module",
"description": "This is an example of a Crawlee project.",
"dependencies": {
"apify": "^3.2.6",
"crawlee": "^3.0.0",
"puppeteer": "^24.6.1"
},
"devDependencies": {
"@apify/tsconfig": "^0.1.0",
"@types/node": "^20.0.0",
"tsx": "^4.4.0",
"typescript": "^5.8.3"
},
"scripts": {
"start": "npm run start:dev",
"start:prod": "node dist/main.js",
"start:dev": "tsx src/main.ts",
"build": "tsc",
"test": "echo \"Error: oops, the actor has no tests yet, sad!\" && exit 1"
},
"author": "It's not you it's me",
"license": "ISC"
}

tsconfig.json

{
"extends": "@apify/tsconfig",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"outDir": "dist",
"noUnusedLocals": false,
"lib": ["DOM"],
"skipLibCheck": true
},
"include": ["./src/**/*"],
"exclude": ["node_modules", "dist"]
}

.actor/actor.json

{
"actorSpecification": 1,
"name": "pinterest-ads-scraper",
"title": "Pinterest Ads Scraper",
"description": "Pinterest Ads Scraper",
"version": "0.0",
"meta": {
"templateId": "js-crawlee-puppeteer-chrome"
},
"input": "./input_schema.json",
"dockerfile": "./Dockerfile",
"storages": {
"dataset": {
"actorSpecification": 1,
"views": {
"overview": {
"title": "Overview",
"transformation": {
"fields": [
"pin_id",
"ad_details"
]
},
"display": {
"component": "table",
"properties": {
"pin_id": {
"format": "string",
"label": "Pin ID"
},
"ad_details": {
"format": "object",
"label": "Details"
}
}
}
}
}
}
}
}

.actor/input_schema.json

{
"title": "Pinterest Ads Scraper",
"type": "object",
"schemaVersion": 1,
"properties": {
"country": {
"title": "Country",
"type": "string",
"description": "Ad Country",
"editor": "select",
"prefill": "FR",
"enumTitles": [
"Austria",
"Belgium",
"Bulgaria",
"Brazil",
"Croatia",
"Cyprus",
"Czech Republic",
"Denmark",
"Estonia",
"Finland",
"France",
"Germany",
"Greece",
"Hungary",
"Ireland",
"Italy",
"Latvia",
"Lithuania",
"Luxembourg",
"Malta",
"Netherlands",
"Poland",
"Portugal",
"Romania",
"Slovakia",
"Slovenia",
"Spain",
"Sweden",
"Turkey"
],
"enum": [
"AT",
"BE",
"BG",
"BR",
"HR",
"CY",
"CZ",
"DK",
"EE",
"FI",
"FR",
"DE",
"GR",
"HU",
"IE",
"IT",
"LV",
"LT",
"LU",
"MT",
"NL",
"PL",
"PT",
"RO",
"SK",
"SI",
"ES",
"SE",
"TR"
]
},
"start_date": {
"title": "Start Date",
"type": "string",
"description": "Select absolute date in format YYYY-MM-DD, The date range must be less than 30 days.",
"editor": "datepicker",
"pattern": "^(\\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$",
"prefill": "2025-04-01"
},
"end_date": {
"title": "End Date",
"type": "string",
"description": "Select absolute date in format YYYY-MM-DD, The date range must be less than 30 days.",
"editor": "datepicker",
"pattern": "^(\\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$",
"prefill": "2025-04-12"
},
"category": {
"title": "Category",
"type": "string",
"description": "Ad Category",
"editor": "select",
"prefill": "ALL",
"enumTitles": [
"All Categories",
"Animals",
"Architecture",
"Art",
"Beauty",
"Children's fashion",
"Design",
"DIY and crafts",
"Education",
"Electronics",
"Entertainment",
"Event planning",
"Finance",
"Food and drinks",
"Health",
"Home decor",
"Gardening",
"Men's fashion",
"Parenting",
"Quotes",
"Sport",
"Travel",
"Vehicles",
"Wedding",
"Women's fashion",
"Other"
],
"enum": [
"ALL",
"ANIMALS",
"ARCHITECTURE",
"ART",
"BEAUTY",
"CHILDRENS_FASHION",
"DESIGN",
"DIY_AND_CRAFTS",
"EDUCATION",
"ELECTRONICS",
"ENTERTAINMENT",
"EVENT_PLANNING",
"FINANCE",
"FOOD_AND_DRINKS",
"HEALTH",
"HOME_DECOR",
"GARDENING",
"MENS_FASHION",
"PARENTING",
"QUOTES",
"SPORT",
"TRAVEL",
"VEHICLES",
"WEDDING",
"WOMENS_FASHION",
"OTHER"
]
},
"gender": {
"title": "Gender",
"type": "string",
"description": "Ad Gender",
"editor": "select",
"prefill": "ALL",
"enumTitles": ["All Genders", "Male", "Female", "Unspecified"],
"enum": ["ALL", "MALE", "FEMALE", "UNSPECIFIED"]
},
"age": {
"title": "Age",
"type": "string",
"description": "Ad Age",
"editor": "select",
"prefill": "ALL",
"enumTitles": [
"All Ages",
"18-24",
"21-24",
"18-20",
"25-34",
"35-44",
"45-49",
"50-54",
"55-64",
"65+"
],
"enum": [
"ALL",
"AGE_18_24",
"AGE_21_24",
"AGE_18_20",
"AGE_25_34",
"AGE_35_44",
"AGE_45_49",
"AGE_50_54",
"AGE_55_64",
"AGE_65_PLUS"
]
},
"results_limit": {
"title": "Results Limit",
"type": "integer",
"editor": "number",
"description": "Limit number of results.",
"prefill": 10
}
},
"required": ["start_date", "end_date", "country"]
}

src/interfaces.ts

1export interface InputSchema {
2 start_date: string;
3 end_date: string;
4 country: string;
5 category?: string;
6 age?: string;
7 gender?: string;
8 results_limit: number;
9}
10
11export interface PinterestAdInterface {
12 pin_id: string;
13 ad_details: {
14 advertiser_names: string | null;
15 statement_of_reasons: string | null;
16 review_status: string | null;
17 violation_source: string | null;
18 violation_decision_means: string | null;
19 start_date: string;
20 end_date: string;
21 age_buckets: object;
22 genders: object;
23 postal_codes: object;
24 metros: object;
25 regions: object;
26 countries: object;
27 content_commercial: boolean;
28 interests: object;
29 pinner_list_types: object;
30 user_count_by_country: object;
31 user_count_eu: string;
32 keywords_used: boolean;
33 negative_keywords_used: boolean;
34 image_link: string;
35 pin_data: {
36 image_link: string;
37 video_link: string | null;
38 details: string;
39 title: string;
40 content_creator_name: string | null;
41 story_pin_page_blocks: object;
42 };
43 };
44}

src/main.ts

1import { Actor } from 'apify';
2import { CheerioCrawler, RequestQueue } from 'crawlee';
3import { InputSchema } from './interfaces.js';
4import { router } from './routes.js';
5
6Actor.main(async () => {
7 let defaultInput: any = {
8 start_date: '2025-04-01',
9 end_date: '2024-04-12',
10 country: 'FR',
11 category: 'ANIMALS',
12 age: 'AGE_18_24',
13 gender: 'MALE',
14 results_limit: 50,
15 };
16
17 let input: InputSchema | null = await Actor.getInput();
18 if (input == null) input = defaultInput;
19
20 // pinterest main url
21 const pinterestAdsUrl = 'https://ads.pinterest.com/ads-repository/';
22
23 const requestQueue = await RequestQueue.open();
24 await requestQueue.addRequest({
25 url: pinterestAdsUrl,
26 label: 'pinterest-ads',
27 userData: {
28 start_date: input?.start_date,
29 end_date: input?.end_date,
30 country: input?.country,
31 category: input?.category,
32 age: input?.age,
33 gender: input?.gender,
34 resultLimit: input?.results_limit,
35 },
36 });
37
38 const crawler = new CheerioCrawler({
39 requestHandler: router,
40 requestQueue,
41 });
42
43 await crawler.run();
44});

src/mappers.ts

1export function getCountries(): string[] {
2 const labels = [
3 'Austria',
4 'Belgium',
5 'Bulgaria',
6 'Brazil',
7 'Croatia',
8 'Cyprus',
9 'Czech Republic',
10 'Denmark',
11 'Estonia',
12 'Finland',
13 'France',
14 'Germany',
15 'Greece',
16 'Hungary',
17 'Ireland',
18 'Italy',
19 'Latvia',
20 'Lithuania',
21 'Luxembourg',
22 'Malta',
23 'Netherlands',
24 'Poland',
25 'Portugal',
26 'Romania',
27 'Slovakia',
28 'Slovenia',
29 'Spain',
30 'Sweden',
31 'Turkey',
32 ];
33 const values = [
34 'AT',
35 'BE',
36 'BG',
37 'BR',
38 'HR',
39 'CY',
40 'CZ',
41 'DK',
42 'EE',
43 'FI',
44 'FR',
45 'DE',
46 'GR',
47 'HU',
48 'IE',
49 'IT',
50 'LV',
51 'LT',
52 'LU',
53 'MT',
54 'NL',
55 'PL',
56 'PT',
57 'RO',
58 'SK',
59 'SI',
60 'ES',
61 'SE',
62 'TR',
63 ];
64 return values;
65}
66
67export function getCategories(): string[] {
68 const labels = [
69 'All Categories',
70 'Animals',
71 'Architecture',
72 'Art',
73 'Beauty',
74 "Children's fashion",
75 'Design',
76 'DIY and crafts',
77 'Education',
78 'Electronics',
79 'Entertainment',
80 'Event planning',
81 'Finance',
82 'Food and drinks',
83 'Health',
84 'Home decor',
85 'Gardening',
86 "Men's fashion",
87 'Parenting',
88 'Quotes',
89 'Sport',
90 'Travel',
91 'Vehicles',
92 'Wedding',
93 "Women's fashion",
94 'Other',
95 ];
96
97 const values = [
98 'ALL',
99 'ANIMALS',
100 'ARCHITECTURE',
101 'ART',
102 'BEAUTY',
103 'CHILDRENS_FASHION',
104 'DESIGN',
105 'DIY_AND_CRAFTS',
106 'EDUCATION',
107 'ELECTRONICS',
108 'ENTERTAINMENT',
109 'EVENT_PLANNING',
110 'FINANCE',
111 'FOOD_AND_DRINKS',
112 'HEALTH',
113 'HOME_DECOR',
114 'GARDENING',
115 'MENS_FASHION',
116 'PARENTING',
117 'QUOTES',
118 'SPORT',
119 'TRAVEL',
120 'VEHICLES',
121 'WEDDING',
122 'WOMENS_FASHION',
123 'OTHER',
124 ];
125
126 return values;
127}
128
129export function getAges(): string[] {
130 let labels = [
131 'All Ages',
132 '18-24',
133 '21-24',
134 '18-20',
135 '25-34',
136 '35-44',
137 '45-49',
138 '50-54',
139 '55-64',
140 '65+',
141 ];
142 let values = [
143 'ALL',
144 'AGE_18_24',
145 'AGE_21_24',
146 'AGE_18_20',
147 'AGE_25_34',
148 'AGE_35_44',
149 'AGE_45_49',
150 'AGE_50_54',
151 'AGE_55_64',
152 'AGE_65_PLUS',
153 ];
154 return values;
155}
156
157export function getGenders(): string[] {
158 let labels = ['All Genders', 'male', 'female', 'unspecified'];
159 let values = ['ALL', 'MALE', 'FEMALE', 'UNSPECIFIED'];
160 return values;
161}

src/routes.ts

1import { createCheerioRouter } from 'crawlee';
2import {
3 delay,
4 formatDate,
5 getDateDiffInDays,
6 getDateSinceNumberOfDays,
7} from './utils.js';
8import { PinterestAdInterface } from './interfaces.js';
9export const router = createCheerioRouter();
10
11/* Constants */
12const HEADERS = {
13 Accept: 'application/json, text/javascript, */*, q=0.01',
14 'x-b3-flags': '0',
15 'x-app-version': '69a5e51',
16 'screen-dpr': '1.25',
17 'x-pinterest-pws-handler': 'sterling/ads-repository.js', // important
18 'User-Agent':
19 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0',
20};
21/* End of Constants*/
22
23/* Main Handlers */
24router.addHandler(
25 'pinterest-ads',
26 async ({ log, request, sendRequest, pushData }) => {
27 let { resultLimit, start_date, end_date, country, category, age, gender } =
28 request.userData;
29
30 // default value for start_date
31 if (!start_date) start_date = formatDate(getDateSinceNumberOfDays(7));
32 // default value for end_date
33 if (!end_date) end_date = formatDate();
34
35 if (getDateDiffInDays(start_date, end_date) >= 30)
36 throw 'The date range must be less than 30 days.';
37
38 // get pinterest ads
39 let numOfAds = 0;
40 let bookmark: string | null = null;
41 while (numOfAds < resultLimit) {
42 // API PARAMS
43 let options: any = {
44 url: '/ads/v4/ads_repository/ad_library/',
45 data: {
46 start_date: start_date,
47 end_date: end_date,
48 country: country,
49 page_size: 24,
50 },
51 };
52 // pagination
53 if (bookmark) options.data.bookmark = bookmark;
54 // filter
55 if (category && category !== 'ALL') options.data.vertical = category;
56 if (gender && gender !== 'ALL') options.data.gender = gender;
57 if (age && age !== 'ALL') options.data.age_bucket = age;
58
59 const queryParams = new URLSearchParams({
60 data: JSON.stringify({
61 options: options,
62 context: {},
63 }),
64 });
65
66 let pinterestAdsApiUrl = `https://ads.pinterest.com/resource/ApiResource/get/`;
67 pinterestAdsApiUrl = `${pinterestAdsApiUrl}?${queryParams.toString()}`;
68 const { body, statusCode } = await sendRequest({
69 method: 'GET',
70 url: pinterestAdsApiUrl,
71 headers: HEADERS,
72 });
73 const pinterestAdsResponse = JSON.parse(body);
74 if (
75 statusCode === 200 &&
76 pinterestAdsResponse?.resource_response?.status === 'success'
77 ) {
78 let ads: PinterestAdInterface[] =
79 pinterestAdsResponse.resource_response.data.pin_ids_with_metadata;
80 for (let item of ads) {
81 if (numOfAds < resultLimit) {
82 log.info(`-> ${item.pin_id}`);
83 numOfAds++;
84 await pushData(item); // for handling apify dataset insertion
85 }
86 }
87 bookmark = pinterestAdsResponse.resource_response.bookmark;
88 if (!bookmark) break;
89 } else {
90 throw 'something went wrong while getting pinterest ads data';
91 }
92 await delay(1000); // delay for a second
93 }
94 }
95);
96/* End of Main Handlers */

src/utils.ts

1export async function delay(time: number): Promise<void> {
2 return new Promise(function (resolve) {
3 setTimeout(resolve, time);
4 });
5}
6
7// Date Helpers
8export function getDateSinceNumberOfDays(days: number = 0): Date {
9 const date = new Date();
10 return new Date(date.getFullYear(), date.getMonth(), date.getDate() - days);
11}
12
13export function formatDate(dateInput: Date | string = new Date()): string {
14 const date = new Date(dateInput);
15 const year = date.getFullYear();
16 const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-based
17 const day = String(date.getDate()).padStart(2, '0');
18 return `${year}-${month}-${day}`;
19}
20
21export function getDateDiffInDays(
22 date1: Date | string,
23 date2: Date | string
24): number {
25 const d1 = new Date(date1);
26 const d2 = new Date(date2);
27 const diffTime = Math.abs(d2.getTime() - d1.getTime()); // difference in milliseconds
28 const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
29 return diffDays;
30}