Skip to main content
RapidDev - Software Development Agency
replit-integrationsStandard API Integration

How to Integrate Replit with Pinterest Ads

To integrate Replit with Pinterest Ads, create a Pinterest app in the developer portal to get OAuth 2.0 credentials, store them in Replit Secrets (lock icon 🔒), and call the Pinterest API v5 from your Python or Node.js server to manage pins, boards, ad campaigns, and conversion tracking. Use Autoscale deployment for campaign management tools.

What you'll learn

  • How to register a Pinterest app and complete the OAuth 2.0 flow
  • How to store Pinterest OAuth credentials securely in Replit Secrets
  • How to manage pins and boards programmatically using Python and Node.js
  • How to create and manage Pinterest ad campaigns and ad groups
  • How to retrieve campaign performance analytics from the Pinterest Ads API
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read35 minutesMarketingMarch 2026RapidDev Engineering Team
TL;DR

To integrate Replit with Pinterest Ads, create a Pinterest app in the developer portal to get OAuth 2.0 credentials, store them in Replit Secrets (lock icon 🔒), and call the Pinterest API v5 from your Python or Node.js server to manage pins, boards, ad campaigns, and conversion tracking. Use Autoscale deployment for campaign management tools.

Why Connect Replit to Pinterest Ads?

Pinterest occupies a unique position in the advertising landscape: its users actively save content with the intent to act on it later — buying products, trying recipes, planning home renovations. This purchase intent makes Pinterest's click-through rates among the highest of any social platform for e-commerce advertisers. The Pinterest API v5 gives programmatic access to everything needed to run a sophisticated Pinterest marketing operation: creating and scheduling pins, managing boards, building ad campaigns, setting targeting parameters, and pulling performance analytics.

The most impactful integration patterns are product catalog syncing (automatically creating pins for new products as they are added to an e-commerce platform), campaign automation (creating and adjusting ad campaigns based on inventory levels or promotional calendars), and analytics pipelines (pulling daily performance data into a reporting database). Connecting your Replit app to Pinterest means these workflows run automatically without manual pin creation or campaign management in the Pinterest Ads UI.

Replit's Secrets system (lock icon 🔒 in the sidebar) is essential because Pinterest uses OAuth 2.0, which requires both a client secret and a refresh token. The client secret must never appear in client-side code, and the refresh token grants long-term access to the Pinterest account. Store both in Replit Secrets and implement token refresh logic in your server — access tokens expire after one hour.

Integration method

Standard API Integration

You connect Replit to Pinterest Ads by registering an application in the Pinterest developer portal to get OAuth 2.0 client credentials, storing client ID, client secret, and refresh token in Replit Secrets, and calling the Pinterest API v5 from your server-side Python or Node.js code. OAuth access tokens expire after one hour and must be refreshed using the stored refresh token. The Pinterest API v5 base URL is https://api.pinterest.com/v5.

Prerequisites

  • A Replit account with a Python or Node.js project created
  • A Pinterest business account (required for API access and ad management)
  • Access to the Pinterest developer portal at developers.pinterest.com
  • Basic familiarity with OAuth 2.0 authorization flows
  • Python 3.10+ or Node.js 18+ (both available on Replit by default)

Step-by-step guide

1

Register a Pinterest App and Complete the OAuth Flow

Visit developers.pinterest.com and sign in with your Pinterest business account. Click 'My apps' in the top navigation and then 'Connect app'. Fill in the app name (e.g., 'Replit Integration'), description, and the redirect URI for your Replit app. For development, use https://your-app.replit.app/oauth/callback. Accept the developer agreement and click 'Submit'. After approval (usually immediate for personal apps), Pinterest provides your App ID (client ID) and App Secret Key (client secret). Copy both values. The Pinterest OAuth flow requires the user (you) to authorize the app once. The authorization URL format is: https://www.pinterest.com/oauth/?client_id={APP_ID}&redirect_uri={REDIRECT_URI}&response_type=code&scope=pins:read,pins:write,boards:read,boards:write,ads:read,ads:write Visit this URL in your browser, authorize your app, and Pinterest redirects to your callback URL with a code parameter. Exchange this code for tokens using POST /v5/oauth/token with grant_type=authorization_code. The response includes an access_token (expires in 1 hour) and a refresh_token (expires after 365 days by default). Store all credentials in Replit Secrets: PINTEREST_CLIENT_ID, PINTEREST_CLIENT_SECRET, and PINTEREST_REFRESH_TOKEN.

Pro tip: Request only the scopes you actually need. For read-only analytics, request ads:read only. Add pins:write and boards:write only if your app creates content. Using minimal scopes reduces the risk of accidental data modification.

Expected result: You have a PINTEREST_CLIENT_ID, PINTEREST_CLIENT_SECRET, and PINTEREST_REFRESH_TOKEN stored in Replit Secrets. A test API call to GET /v5/user_account confirms the credentials work.

2

Implement Token Refresh and Pin Management in Python

Pinterest access tokens expire after one hour, so you must implement token refresh logic. The refresh endpoint accepts grant_type=refresh_token with the stored refresh token using the client credentials. Like most OAuth implementations, the refresh uses HTTP Basic Auth with the client ID and secret. The Python module below handles the complete token lifecycle with an in-memory cache and automatic refresh. It covers the core Pinterest content management operations: creating pins with images and links, managing boards, and fetching existing content. The Pinterest API v5 uses HTTPS throughout and requires all media content to be referenced by URL — you cannot upload binary files directly. Images for pins must be hosted on a publicly accessible URL (CDN, S3, or your server). For ad creatives, ensure images meet Pinterest's specifications: minimum 600px width and 2:3 aspect ratio is recommended for standard pin format.

pinterest_client.py
1import os
2import time
3import requests
4from base64 import b64encode
5from typing import Optional
6
7CLIENT_ID = os.environ["PINTEREST_CLIENT_ID"]
8CLIENT_SECRET = os.environ["PINTEREST_CLIENT_SECRET"]
9REFRESH_TOKEN = os.environ["PINTEREST_REFRESH_TOKEN"]
10BASE_URL = "https://api.pinterest.com/v5"
11TOKEN_URL = f"{BASE_URL}/oauth/token"
12
13# In-memory token cache
14_token_cache = {"access_token": None, "expires_at": 0}
15
16def get_access_token() -> str:
17 """Get a valid access token, refreshing if expired or missing."""
18 if time.time() < _token_cache["expires_at"] - 60:
19 return _token_cache["access_token"]
20
21 credentials = b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
22 headers = {
23 "Authorization": f"Basic {credentials}",
24 "Content-Type": "application/x-www-form-urlencoded"
25 }
26 data = {
27 "grant_type": "refresh_token",
28 "refresh_token": REFRESH_TOKEN,
29 "scope": "pins:read pins:write boards:read boards:write ads:read ads:write"
30 }
31 response = requests.post(TOKEN_URL, headers=headers, data=data)
32 response.raise_for_status()
33 token_data = response.json()
34
35 _token_cache["access_token"] = token_data["access_token"]
36 _token_cache["expires_at"] = time.time() + token_data.get("expires_in", 3600)
37 return _token_cache["access_token"]
38
39def api_headers() -> dict:
40 return {
41 "Authorization": f"Bearer {get_access_token()}",
42 "Content-Type": "application/json"
43 }
44
45def get_user_boards() -> list:
46 """Get all boards belonging to the authenticated user."""
47 response = requests.get(f"{BASE_URL}/boards", headers=api_headers())
48 response.raise_for_status()
49 return response.json().get("items", [])
50
51def create_pin(board_id: str, title: str, description: str,
52 image_url: str, link: str = "") -> dict:
53 """Create a new pin on the specified board."""
54 payload = {
55 "board_id": board_id,
56 "title": title,
57 "description": description,
58 "media_source": {
59 "source_type": "image_url",
60 "url": image_url
61 }
62 }
63 if link:
64 payload["link"] = link
65
66 response = requests.post(f"{BASE_URL}/pins", json=payload, headers=api_headers())
67 response.raise_for_status()
68 return response.json()
69
70def get_ad_accounts() -> list:
71 """List all ad accounts accessible to the authenticated user."""
72 response = requests.get(f"{BASE_URL}/ad_accounts", headers=api_headers())
73 response.raise_for_status()
74 return response.json().get("items", [])
75
76def get_campaign_analytics(ad_account_id: str, campaign_ids: list,
77 start_date: str, end_date: str) -> dict:
78 """Get analytics for specific campaigns."""
79 params = {
80 "start_date": start_date, # YYYY-MM-DD
81 "end_date": end_date,
82 "campaign_ids": campaign_ids,
83 "columns": "SPEND_IN_DOLLAR,IMPRESSION_1,CLICK_1,SAVE",
84 "granularity": "DAY"
85 }
86 response = requests.get(
87 f"{BASE_URL}/ad_accounts/{ad_account_id}/campaigns/analytics",
88 params=params,
89 headers=api_headers()
90 )
91 response.raise_for_status()
92 return response.json()
93
94# Example usage
95if __name__ == "__main__":
96 boards = get_user_boards()
97 print("Your boards:")
98 for board in boards:
99 print(f" {board['id']}: {board['name']}")
100
101 ad_accounts = get_ad_accounts()
102 if ad_accounts:
103 print(f"\nAd account: {ad_accounts[0]['id']}")

Pro tip: Pinterest pin images must be hosted on a publicly accessible URL. If you want to pin images from your app, upload them to AWS S3, Cloudflare R2, or another public CDN first, then pass the public URL to the pin creation API.

Expected result: Running the script prints your Pinterest boards and ad accounts. The token refresh runs automatically on first execution and caches the access token for subsequent calls.

3

Build a Node.js Campaign Management Server

The Node.js integration implements the same OAuth token refresh pattern as Python. The Express server exposes endpoints for creating pins in bulk (useful for product catalog publishing), fetching campaign analytics, and updating campaign status. The campaign analytics endpoint is particularly valuable for automated reporting workflows. Pinterest returns metrics broken down by day and campaign, which you can aggregate into weekly or monthly summaries. The metrics include impressions, link clicks, saves (re-pins), video views (for video pins), and conversion events if you have the Pinterest Tag installed on your website. Install dependencies with 'npm install express axios' in the Replit shell. The server handles token refresh internally before each API call using the same cache pattern as the Python version.

server.js
1const express = require('express');
2const axios = require('axios');
3const { Buffer } = require('buffer');
4
5const app = express();
6app.use(express.json());
7
8const CLIENT_ID = process.env.PINTEREST_CLIENT_ID;
9const CLIENT_SECRET = process.env.PINTEREST_CLIENT_SECRET;
10const REFRESH_TOKEN = process.env.PINTEREST_REFRESH_TOKEN;
11const BASE_URL = 'https://api.pinterest.com/v5';
12
13let tokenCache = { accessToken: null, expiresAt: 0 };
14
15async function getAccessToken() {
16 if (Date.now() / 1000 < tokenCache.expiresAt - 60) {
17 return tokenCache.accessToken;
18 }
19 const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
20 const response = await axios.post(
21 `${BASE_URL}/oauth/token`,
22 new URLSearchParams({
23 grant_type: 'refresh_token',
24 refresh_token: REFRESH_TOKEN,
25 scope: 'pins:read pins:write boards:read ads:read ads:write'
26 }),
27 {
28 headers: {
29 'Authorization': `Basic ${credentials}`,
30 'Content-Type': 'application/x-www-form-urlencoded'
31 }
32 }
33 );
34 tokenCache.accessToken = response.data.access_token;
35 tokenCache.expiresAt = Date.now() / 1000 + (response.data.expires_in || 3600);
36 return tokenCache.accessToken;
37}
38
39async function pinterest() {
40 const token = await getAccessToken();
41 return axios.create({
42 baseURL: BASE_URL,
43 headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
44 });
45}
46
47// Create a pin on a board
48app.post('/pins', async (req, res) => {
49 const { boardId, title, description, imageUrl, link } = req.body;
50 if (!boardId || !imageUrl) {
51 return res.status(400).json({ error: 'boardId and imageUrl are required' });
52 }
53 try {
54 const api = await pinterest();
55 const payload = {
56 board_id: boardId,
57 title: title || '',
58 description: description || '',
59 media_source: { source_type: 'image_url', url: imageUrl }
60 };
61 if (link) payload.link = link;
62 const { data } = await api.post('/pins', payload);
63 res.json({ success: true, pinId: data.id, url: `https://pinterest.com/pin/${data.id}` });
64 } catch (err) {
65 console.error('Pin error:', err.response?.data || err.message);
66 res.status(500).json({ error: err.message });
67 }
68});
69
70// Get campaign analytics
71app.get('/analytics/:adAccountId', async (req, res) => {
72 const { startDate, endDate, campaignIds } = req.query;
73 if (!startDate || !endDate) {
74 return res.status(400).json({ error: 'startDate and endDate are required (YYYY-MM-DD)' });
75 }
76 try {
77 const api = await pinterest();
78 const params = {
79 start_date: startDate,
80 end_date: endDate,
81 columns: 'SPEND_IN_DOLLAR,IMPRESSION_1,CLICK_1,SAVE,TOTAL_CONVERSIONS',
82 granularity: 'TOTAL'
83 };
84 if (campaignIds) params.campaign_ids = campaignIds.split(',');
85
86 const { data } = await api.get(
87 `/ad_accounts/${req.params.adAccountId}/campaigns/analytics`,
88 { params }
89 );
90 res.json(data);
91 } catch (err) {
92 console.error('Analytics error:', err.response?.data || err.message);
93 res.status(500).json({ error: err.message });
94 }
95});
96
97// List boards
98app.get('/boards', async (req, res) => {
99 try {
100 const api = await pinterest();
101 const { data } = await api.get('/boards');
102 res.json(data.items || []);
103 } catch (err) {
104 res.status(500).json({ error: err.message });
105 }
106});
107
108app.get('/health', (req, res) => res.json({ status: 'ok' }));
109
110app.listen(3000, '0.0.0.0', () => {
111 console.log('Pinterest integration server running on port 3000');
112});

Pro tip: The Pinterest API rate limits vary by endpoint. Analytics endpoints are limited to 1 request per second. For bulk analytics pulls across many campaigns, add a delay between requests to avoid 429 rate limit errors.

Expected result: The server starts and GET /boards returns your Pinterest boards. POST /pins creates a test pin and returns the pin URL.

4

Deploy and Set Up Conversion Tracking

Pinterest Conversion API (CAPI) lets you send conversion events from your Replit server directly to Pinterest, bypassing browser-based pixel limitations from ad blockers. This server-side event matching attributes conversions back to the Pinterest ads that drove them. To send conversions, use POST /v5/ad_accounts/{ad_account_id}/events with the event type (checkout, add_to_cart, page_visit, etc.), user data for matching (hashed email, IP, user agent), and event details (value, currency, order ID). Hash all PII with SHA-256 before sending — Pinterest requires hashed data. Deploy your Replit app by clicking 'Deploy' and selecting Autoscale. Autoscale is appropriate for Pinterest integrations that fire during user sessions (conversions, catalog updates). After deployment, copy your stable URL and add it to Pinterest developer portal redirect URIs if you need to re-authorize. Monitor the Replit deployment logs for token refresh errors that would stop all API calls.

pinterest_conversions.py
1import hashlib
2import requests
3from datetime import datetime
4import os
5
6AD_ACCOUNT_ID = os.environ["PINTEREST_AD_ACCOUNT_ID"]
7
8def sha256_hash(value: str) -> str:
9 """Hash PII data before sending to Pinterest API."""
10 return hashlib.sha256(value.lower().strip().encode()).hexdigest()
11
12def send_conversion_event(event_name: str, email: str,
13 order_value: float, order_id: str,
14 ip_address: str = "", user_agent: str = "") -> dict:
15 """
16 Send a server-side conversion event to Pinterest.
17 event_name: checkout, add_to_cart, page_visit, signup, lead
18 """
19 payload = {
20 "data": [
21 {
22 "event_name": event_name,
23 "action_source": "web",
24 "event_time": int(datetime.now().timestamp()),
25 "event_id": order_id,
26 "user_data": {
27 "em": [sha256_hash(email)], # Hashed email
28 "client_ip_address": ip_address,
29 "client_user_agent": user_agent
30 },
31 "custom_data": {
32 "currency": "USD",
33 "value": str(order_value),
34 "order_id": order_id
35 }
36 }
37 ]
38 }
39
40 response = requests.post(
41 f"https://api.pinterest.com/v5/ad_accounts/{AD_ACCOUNT_ID}/events",
42 json=payload,
43 headers=api_headers() # Using the token from Step 2
44 )
45 response.raise_for_status()
46 return response.json()
47
48# Example: send a checkout conversion
49if __name__ == "__main__":
50 result = send_conversion_event(
51 event_name="checkout",
52 email="customer@example.com",
53 order_value=129.99,
54 order_id="ORD-2024-001"
55 )
56 print(f"Conversion event sent: {result}")

Pro tip: Always hash email addresses, phone numbers, and other PII with SHA-256 before sending to the Pinterest Conversion API. Pinterest requires hashed data to match conversions to ad interactions while complying with privacy regulations.

Expected result: Conversion events appear in your Pinterest Ads account under the Conversions section. The event should show as received within a few minutes of the API call.

Common use cases

Automated Product Pin Creation from E-Commerce Catalog

When new products are added to your e-commerce database, a Replit server automatically creates Pinterest pins with the product image, title, description, and link back to the product page. New products reach Pinterest's discovery engine within minutes of being listed, without any manual pin creation by the marketing team.

Replit Prompt

Build a Flask endpoint that receives new product data (name, image URL, description, product URL) and creates a Pinterest pin on the product catalog board using PINTEREST_ACCESS_TOKEN from Replit Secrets.

Copy this prompt to try it in Replit

Automated Ad Campaign Performance Reports

A Replit script runs daily to pull Pinterest ad campaign metrics — impressions, clicks, saves, conversions, and spend — and writes them to a Google Sheet or database. Marketing managers see yesterday's Pinterest performance in their daily dashboard without logging into the Pinterest Ads Manager.

Replit Prompt

Write a Python script that fetches Pinterest ad campaign analytics for the previous day using the Ads API, including impressions, clicks, and spend per campaign, and prints a formatted performance summary.

Copy this prompt to try it in Replit

Dynamic Campaign Budget Adjustment

A Replit automation monitors campaign performance metrics and automatically adjusts daily budgets based on return on ad spend. Campaigns performing above a target ROAS threshold get budget increases, while underperforming campaigns have their budgets reduced or are paused — all without manual advertiser intervention.

Replit Prompt

Create a Node.js script that fetches Pinterest campaign performance for the past 7 days, calculates ROAS for each campaign, and uses the Pinterest API to increase the daily budget by 20% for campaigns with ROAS above 3.0.

Copy this prompt to try it in Replit

Troubleshooting

OAuth token refresh returns 'invalid_client' or 401

Cause: The client ID and client secret are being sent in the request body instead of as HTTP Basic Auth, or the client secret has extra whitespace from copy-paste.

Solution: Pinterest requires client credentials in the Authorization: Basic header, not in the request body. Base64-encode 'clientId:clientSecret' and set it as the Authorization header. Check PINTEREST_CLIENT_SECRET in Replit Secrets for extra spaces.

typescript
1# Python: correct Basic Auth encoding for token refresh
2from base64 import b64encode
3credentials = b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
4headers = {"Authorization": f"Basic {credentials}"}

Pin creation returns 400 with 'Invalid image URL'

Cause: The image URL is not publicly accessible, returns a non-200 status, is behind authentication, or the content type is not a supported image format. Pinterest fetches the image when the pin is created.

Solution: Ensure the image URL is publicly accessible without authentication. The URL must return an image file (JPEG, PNG, WebP) directly, not an HTML page. Test the URL in a private browser window to confirm it is accessible. Pinterest recommends images at least 600px wide.

GET /ad_accounts returns empty list even though you have an ad account

Cause: The OAuth scopes used during authorization did not include 'ads:read'. Pinterest requires explicit scope authorization for ad account access, and missing scopes result in empty responses rather than permission errors.

Solution: Re-authorize the OAuth flow with the correct scopes including 'ads:read' and 'ads:write'. Generate a new authorization URL with all required scopes, complete the flow, and store the new refresh token in PINTEREST_REFRESH_TOKEN.

typescript
1# Required scopes for full access
2scope = 'pins:read pins:write boards:read boards:write ads:read ads:write'
3auth_url = f"https://www.pinterest.com/oauth/?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&response_type=code&scope={scope}"

Analytics endpoint returns 429 Too Many Requests

Cause: The Pinterest Analytics API enforces a rate limit of approximately 1 request per second per token. Fetching analytics for many campaigns in a loop without delays triggers rate limiting.

Solution: Add a 1-second delay between successive analytics API calls. For bulk analytics pulls, batch campaign IDs into groups and pause between batches. Cache analytics results in memory or a database for repeated reads rather than re-fetching from the API.

typescript
1import time
2
3# Add delay between analytics requests
4for campaign_id in campaign_ids:
5 analytics = get_campaign_analytics(ad_account_id, [campaign_id], start, end)
6 results.append(analytics)
7 time.sleep(1.0) # 1 second between requests

Best practices

  • Store PINTEREST_CLIENT_ID, PINTEREST_CLIENT_SECRET, and PINTEREST_REFRESH_TOKEN in Replit Secrets (lock icon 🔒) — the client secret must never appear in frontend code or Git history.
  • Implement access token refresh logic with an in-memory cache and expiry timestamp to avoid making a token refresh call before every single API request.
  • Request only the OAuth scopes you need — request ads:read only for analytics tools, and add pins:write only if your app creates content.
  • Hash all PII (email, phone number) with SHA-256 before sending to the Pinterest Conversion API — this is required by Pinterest's API and privacy regulations.
  • Host pin images on a public CDN or object storage service before creating pins — Pinterest fetches images by URL and rejects inaccessible or private URLs.
  • Respect the analytics API rate limit of approximately 1 request per second — add delays between calls and cache results to avoid 429 errors.
  • Deploy with Autoscale for catalog publishing and conversion tracking workflows, as these are triggered by user events rather than running continuously.
  • Monitor your refresh token expiry (365 days by default) — set a calendar reminder to re-authorize before it expires to prevent integration downtime.

Alternatives

Frequently asked questions

How do I store Pinterest credentials in Replit?

Click the lock icon 🔒 in the left sidebar of your Replit project. Add PINTEREST_CLIENT_ID, PINTEREST_CLIENT_SECRET, and PINTEREST_REFRESH_TOKEN as separate secrets. The refresh token is obtained after completing the OAuth authorization flow once. Access them in Python with os.environ['PINTEREST_CLIENT_ID'] and in Node.js with process.env.PINTEREST_CLIENT_ID.

Does Replit work with Pinterest API on the free plan?

Yes. The Pinterest API is accessible from Replit's free tier for outbound calls. Pinterest requires a business account to access the Ads API — personal accounts cannot run ad campaigns. Pinterest API access itself is free through the developer portal. Replit paid plans are needed for always-on deployment.

How long do Pinterest OAuth tokens last?

Pinterest access tokens expire after one hour (3600 seconds). Refresh tokens last for 365 days by default and can be used to obtain new access tokens without requiring user re-authorization. Always implement token refresh logic that checks expiry before each API call and uses the refresh token when the access token has expired.

Can I create pins in bulk from Replit?

Yes. Call POST /v5/pins for each pin you want to create. Include the board_id, title, description, and a media_source with an image_url pointing to a publicly accessible image. For bulk creation, add small delays (0.5-1 second) between requests to stay within Pinterest's rate limits.

What Pinterest API scopes do I need for ad campaign management?

For full ad campaign access, request the following scopes during OAuth authorization: ads:read (view campaign data and analytics), ads:write (create and modify campaigns), pins:read, pins:write, boards:read, and boards:write. You can start with ads:read only for analytics-only integrations, then re-authorize with write scopes when you need to create or modify campaigns.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.