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
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
Register a Pinterest App and Complete the OAuth Flow
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.
Implement Token Refresh and Pin Management in Python
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.
1import os2import time3import requests4from base64 import b64encode5from typing import Optional67CLIENT_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"1213# In-memory token cache14_token_cache = {"access_token": None, "expires_at": 0}1516def 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"]2021 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()3435 _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"]3839def api_headers() -> dict:40 return {41 "Authorization": f"Bearer {get_access_token()}",42 "Content-Type": "application/json"43 }4445def 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", [])5051def 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_url61 }62 }63 if link:64 payload["link"] = link6566 response = requests.post(f"{BASE_URL}/pins", json=payload, headers=api_headers())67 response.raise_for_status()68 return response.json()6970def 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", [])7576def 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-DD81 "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()9394# Example usage95if __name__ == "__main__":96 boards = get_user_boards()97 print("Your boards:")98 for board in boards:99 print(f" {board['id']}: {board['name']}")100101 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.
Build a Node.js Campaign Management Server
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.
1const express = require('express');2const axios = require('axios');3const { Buffer } = require('buffer');45const app = express();6app.use(express.json());78const 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';1213let tokenCache = { accessToken: null, expiresAt: 0 };1415async 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}3839async 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}4647// Create a pin on a board48app.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});6970// Get campaign analytics71app.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(',');8586 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});9697// List boards98app.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});107108app.get('/health', (req, res) => res.json({ status: 'ok' }));109110app.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.
Deploy and Set Up Conversion Tracking
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.
1import hashlib2import requests3from datetime import datetime4import os56AD_ACCOUNT_ID = os.environ["PINTEREST_AD_ACCOUNT_ID"]78def sha256_hash(value: str) -> str:9 """Hash PII data before sending to Pinterest API."""10 return hashlib.sha256(value.lower().strip().encode()).hexdigest()1112def 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, lead18 """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 email28 "client_ip_address": ip_address,29 "client_user_agent": user_agent30 },31 "custom_data": {32 "currency": "USD",33 "value": str(order_value),34 "order_id": order_id35 }36 }37 ]38 }3940 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 244 )45 response.raise_for_status()46 return response.json()4748# Example: send a checkout conversion49if __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.
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.
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.
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.
1# Python: correct Basic Auth encoding for token refresh2from base64 import b64encode3credentials = 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.
1# Required scopes for full access2scope = '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.
1import time23# Add delay between analytics requests4for 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 requestsBest 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
Facebook Ads reaches a larger and more diverse audience with more advanced demographic targeting and retargeting capabilities, making it the better choice for B2C brands targeting broad consumer demographics.
TikTok Ads is better for reaching Gen Z and Millennial audiences through short-form video content, with higher engagement rates for entertainment and fashion brands than Pinterest's static image format.
LinkedIn Ads is better for B2B campaigns targeting specific job titles, industries, and companies, while Pinterest is stronger for B2C consumer products with visual appeal and purchase intent.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation