# How to Integrate Replit with Pinterest Ads

- Tool: Replit
- Difficulty: Intermediate
- Time required: 35 minutes
- Last updated: March 2026

## 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.

## Before you start

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

**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.

```
import os
import time
import requests
from base64 import b64encode
from typing import Optional

CLIENT_ID = os.environ["PINTEREST_CLIENT_ID"]
CLIENT_SECRET = os.environ["PINTEREST_CLIENT_SECRET"]
REFRESH_TOKEN = os.environ["PINTEREST_REFRESH_TOKEN"]
BASE_URL = "https://api.pinterest.com/v5"
TOKEN_URL = f"{BASE_URL}/oauth/token"

# In-memory token cache
_token_cache = {"access_token": None, "expires_at": 0}

def get_access_token() -> str:
    """Get a valid access token, refreshing if expired or missing."""
    if time.time() < _token_cache["expires_at"] - 60:
        return _token_cache["access_token"]

    credentials = b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
    headers = {
        "Authorization": f"Basic {credentials}",
        "Content-Type": "application/x-www-form-urlencoded"
    }
    data = {
        "grant_type": "refresh_token",
        "refresh_token": REFRESH_TOKEN,
        "scope": "pins:read pins:write boards:read boards:write ads:read ads:write"
    }
    response = requests.post(TOKEN_URL, headers=headers, data=data)
    response.raise_for_status()
    token_data = response.json()

    _token_cache["access_token"] = token_data["access_token"]
    _token_cache["expires_at"] = time.time() + token_data.get("expires_in", 3600)
    return _token_cache["access_token"]

def api_headers() -> dict:
    return {
        "Authorization": f"Bearer {get_access_token()}",
        "Content-Type": "application/json"
    }

def get_user_boards() -> list:
    """Get all boards belonging to the authenticated user."""
    response = requests.get(f"{BASE_URL}/boards", headers=api_headers())
    response.raise_for_status()
    return response.json().get("items", [])

def create_pin(board_id: str, title: str, description: str,
              image_url: str, link: str = "") -> dict:
    """Create a new pin on the specified board."""
    payload = {
        "board_id": board_id,
        "title": title,
        "description": description,
        "media_source": {
            "source_type": "image_url",
            "url": image_url
        }
    }
    if link:
        payload["link"] = link

    response = requests.post(f"{BASE_URL}/pins", json=payload, headers=api_headers())
    response.raise_for_status()
    return response.json()

def get_ad_accounts() -> list:
    """List all ad accounts accessible to the authenticated user."""
    response = requests.get(f"{BASE_URL}/ad_accounts", headers=api_headers())
    response.raise_for_status()
    return response.json().get("items", [])

def get_campaign_analytics(ad_account_id: str, campaign_ids: list,
                           start_date: str, end_date: str) -> dict:
    """Get analytics for specific campaigns."""
    params = {
        "start_date": start_date,   # YYYY-MM-DD
        "end_date": end_date,
        "campaign_ids": campaign_ids,
        "columns": "SPEND_IN_DOLLAR,IMPRESSION_1,CLICK_1,SAVE",
        "granularity": "DAY"
    }
    response = requests.get(
        f"{BASE_URL}/ad_accounts/{ad_account_id}/campaigns/analytics",
        params=params,
        headers=api_headers()
    )
    response.raise_for_status()
    return response.json()

# Example usage
if __name__ == "__main__":
    boards = get_user_boards()
    print("Your boards:")
    for board in boards:
        print(f"  {board['id']}: {board['name']}")

    ad_accounts = get_ad_accounts()
    if ad_accounts:
        print(f"\nAd account: {ad_accounts[0]['id']}")
```

**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.

```
const express = require('express');
const axios = require('axios');
const { Buffer } = require('buffer');

const app = express();
app.use(express.json());

const CLIENT_ID = process.env.PINTEREST_CLIENT_ID;
const CLIENT_SECRET = process.env.PINTEREST_CLIENT_SECRET;
const REFRESH_TOKEN = process.env.PINTEREST_REFRESH_TOKEN;
const BASE_URL = 'https://api.pinterest.com/v5';

let tokenCache = { accessToken: null, expiresAt: 0 };

async function getAccessToken() {
  if (Date.now() / 1000 < tokenCache.expiresAt - 60) {
    return tokenCache.accessToken;
  }
  const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
  const response = await axios.post(
    `${BASE_URL}/oauth/token`,
    new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: REFRESH_TOKEN,
      scope: 'pins:read pins:write boards:read ads:read ads:write'
    }),
    {
      headers: {
        'Authorization': `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }
  );
  tokenCache.accessToken = response.data.access_token;
  tokenCache.expiresAt = Date.now() / 1000 + (response.data.expires_in || 3600);
  return tokenCache.accessToken;
}

async function pinterest() {
  const token = await getAccessToken();
  return axios.create({
    baseURL: BASE_URL,
    headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
  });
}

// Create a pin on a board
app.post('/pins', async (req, res) => {
  const { boardId, title, description, imageUrl, link } = req.body;
  if (!boardId || !imageUrl) {
    return res.status(400).json({ error: 'boardId and imageUrl are required' });
  }
  try {
    const api = await pinterest();
    const payload = {
      board_id: boardId,
      title: title || '',
      description: description || '',
      media_source: { source_type: 'image_url', url: imageUrl }
    };
    if (link) payload.link = link;
    const { data } = await api.post('/pins', payload);
    res.json({ success: true, pinId: data.id, url: `https://pinterest.com/pin/${data.id}` });
  } catch (err) {
    console.error('Pin error:', err.response?.data || err.message);
    res.status(500).json({ error: err.message });
  }
});

// Get campaign analytics
app.get('/analytics/:adAccountId', async (req, res) => {
  const { startDate, endDate, campaignIds } = req.query;
  if (!startDate || !endDate) {
    return res.status(400).json({ error: 'startDate and endDate are required (YYYY-MM-DD)' });
  }
  try {
    const api = await pinterest();
    const params = {
      start_date: startDate,
      end_date: endDate,
      columns: 'SPEND_IN_DOLLAR,IMPRESSION_1,CLICK_1,SAVE,TOTAL_CONVERSIONS',
      granularity: 'TOTAL'
    };
    if (campaignIds) params.campaign_ids = campaignIds.split(',');

    const { data } = await api.get(
      `/ad_accounts/${req.params.adAccountId}/campaigns/analytics`,
      { params }
    );
    res.json(data);
  } catch (err) {
    console.error('Analytics error:', err.response?.data || err.message);
    res.status(500).json({ error: err.message });
  }
});

// List boards
app.get('/boards', async (req, res) => {
  try {
    const api = await pinterest();
    const { data } = await api.get('/boards');
    res.json(data.items || []);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.get('/health', (req, res) => res.json({ status: 'ok' }));

app.listen(3000, '0.0.0.0', () => {
  console.log('Pinterest integration server running on port 3000');
});
```

**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.

```
import hashlib
import requests
from datetime import datetime
import os

AD_ACCOUNT_ID = os.environ["PINTEREST_AD_ACCOUNT_ID"]

def sha256_hash(value: str) -> str:
    """Hash PII data before sending to Pinterest API."""
    return hashlib.sha256(value.lower().strip().encode()).hexdigest()

def send_conversion_event(event_name: str, email: str,
                          order_value: float, order_id: str,
                          ip_address: str = "", user_agent: str = "") -> dict:
    """
    Send a server-side conversion event to Pinterest.
    event_name: checkout, add_to_cart, page_visit, signup, lead
    """
    payload = {
        "data": [
            {
                "event_name": event_name,
                "action_source": "web",
                "event_time": int(datetime.now().timestamp()),
                "event_id": order_id,
                "user_data": {
                    "em": [sha256_hash(email)],  # Hashed email
                    "client_ip_address": ip_address,
                    "client_user_agent": user_agent
                },
                "custom_data": {
                    "currency": "USD",
                    "value": str(order_value),
                    "order_id": order_id
                }
            }
        ]
    }

    response = requests.post(
        f"https://api.pinterest.com/v5/ad_accounts/{AD_ACCOUNT_ID}/events",
        json=payload,
        headers=api_headers()  # Using the token from Step 2
    )
    response.raise_for_status()
    return response.json()

# Example: send a checkout conversion
if __name__ == "__main__":
    result = send_conversion_event(
        event_name="checkout",
        email="customer@example.com",
        order_value=129.99,
        order_id="ORD-2024-001"
    )
    print(f"Conversion event sent: {result}")
```

**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.

## 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.

## 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.

Prompt example:

```
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.
```

### 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.

Prompt example:

```
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.
```

### 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.

Prompt example:

```
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.
```

## 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.

```
# Python: correct Basic Auth encoding for token refresh
from base64 import b64encode
credentials = b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
headers = {"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.

```
# Required scopes for full access
scope = 'pins:read pins:write boards:read boards:write ads:read ads:write'
auth_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.

```
import time

# Add delay between analytics requests
for campaign_id in campaign_ids:
    analytics = get_campaign_analytics(ad_account_id, [campaign_id], start, end)
    results.append(analytics)
    time.sleep(1.0)  # 1 second between requests
```

## 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.

---

Source: https://www.rapidevelopers.com/replit-integration/pinterest-ads
© RapidDev — https://www.rapidevelopers.com/replit-integration/pinterest-ads
