Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Pinterest Ads

Integrate Bolt.new with Pinterest Ads using the Pinterest Marketing API via Next.js API routes. Create a Pinterest developer app, implement OAuth 2.0 to obtain access tokens, then fetch campaign and ad group performance data including impressions, clicks, spend, and conversions. Build a Pinterest Ads analytics dashboard. Pinterest targets visual discovery intent — highly effective for e-commerce, home, fashion, and lifestyle brands targeting purchase-intent audiences.

What you'll learn

  • How to create a Pinterest developer app and configure OAuth 2.0 for Marketing API access
  • How to obtain and refresh Pinterest access tokens for server-side API calls
  • How to fetch campaign, ad group, and promoted pin performance metrics via a Next.js API route
  • How to build a Pinterest Ads dashboard highlighting visual discovery and shopping-intent metrics
  • How to add Pinterest Tag conversion tracking to your deployed Bolt app
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate20 min read35 minutesMarketingApril 2026RapidDev Engineering Team
TL;DR

Integrate Bolt.new with Pinterest Ads using the Pinterest Marketing API via Next.js API routes. Create a Pinterest developer app, implement OAuth 2.0 to obtain access tokens, then fetch campaign and ad group performance data including impressions, clicks, spend, and conversions. Build a Pinterest Ads analytics dashboard. Pinterest targets visual discovery intent — highly effective for e-commerce, home, fashion, and lifestyle brands targeting purchase-intent audiences.

Build a Pinterest Ads Analytics Dashboard in Bolt.new

Pinterest occupies a unique position in digital advertising because its users arrive with explicit purchasing intent. Unlike social media platforms where ads interrupt content consumption, Pinterest's core product is visual discovery — people actively searching for ideas, products, and inspiration. This fundamental difference means Pinterest ad performance data tells a different story than Facebook or Google analytics: save rate (how often users save your pin to a board for later) is as important as click-through rate because a saved pin represents deferred purchase intent.

Pinterest's 465 million monthly active users skew heavily toward specific verticals: home improvement, fashion, food, beauty, and wedding planning are consistently the highest-performing categories. For e-commerce brands in these verticals, Pinterest Ads often achieve ROAS (Return on Ad Spend) comparable to or exceeding Facebook campaigns at lower CPCs. Building a Pinterest Ads analytics dashboard in Bolt lets you surface these shopping-intent metrics in a context tailored to how your business actually measures success.

The Pinterest Marketing API follows standard REST conventions with OAuth 2.0 authentication. Access is available to any developer with a Pinterest Business account — there is no waiting list or manual approval process beyond creating a developer app. The initial OAuth flow requires a redirect URI pointing to a deployed URL, but once you have a refresh token, all subsequent API calls are server-to-server without user interaction. This means you can get credentials, store the refresh token in .env, and the analytics dashboard runs entirely through server-side Next.js API routes that work both in Bolt's WebContainer preview and in production.

Integration method

Bolt Chat + API Route

Bolt generates Next.js API routes that call Pinterest's Marketing API with OAuth 2.0 Bearer tokens, keeping access tokens server-side. Pinterest's API is REST over HTTPS, so outbound calls for reading campaign data work in Bolt's WebContainer preview. OAuth 2.0 authorization requires a deployed redirect URI for the initial token flow, but a refresh token obtained once can be stored in .env for ongoing server-side token renewal without user interaction.

Prerequisites

  • A Bolt.new account with a Next.js project
  • A Pinterest Business account at pinterest.com/business
  • An active Pinterest Ads account with at least one campaign
  • A Pinterest developer app created at developers.pinterest.com with Marketing API access enabled
  • The initial OAuth authorization completed to obtain a refresh token (requires a deployed redirect URI or test URL)

Step-by-step guide

1

Create a Pinterest Developer App and Enable Marketing API

Pinterest's developer program is accessible to any Pinterest Business account holder. Setting up API access is straightforward compared to platforms that require manual approval or partner status. Go to developers.pinterest.com and sign in with your Pinterest account. Click 'Create app' and fill in your app details: name, description, and website URL. Under 'Permissions,' enable the following scopes for your app: `ads:read` for reading campaign data, `ads:write` for creating and modifying campaigns (optional), and `user_accounts:read` for verifying account access. Click 'Create' to submit the app for review. Pinterest app review is typically completed within 24-48 hours for Marketing API access requests with a legitimate business use case. While waiting for review, note your App ID and App Secret from the app detail page — these are your OAuth 2.0 client credentials. In your Bolt project, add these to your .env file. Also add your Pinterest Ad Account ID, which you can find in Pinterest Ads Manager (ads.pinterest.com) — it appears in the URL when you are viewing an ad account, formatted as a numeric ID like `123456789`. Pinterest uses their own OAuth 2.0 implementation rather than a generic provider like Azure AD. The token endpoint is `https://api.pinterest.com/v5/oauth/token` and authorization URL is `https://www.pinterest.com/oauth/`. Access tokens expire after 30 days, and refresh tokens expire after one year but reset on each use, giving effectively indefinite access as long as the dashboard is used regularly.

Bolt.new Prompt

Add to .env: PINTEREST_APP_ID=your-app-id, PINTEREST_APP_SECRET=your-app-secret, PINTEREST_ACCESS_TOKEN=your-access-token, PINTEREST_REFRESH_TOKEN=your-refresh-token, PINTEREST_AD_ACCOUNT_ID=your-account-id. Create lib/pinterest.ts that exports a getPinterestToken function that uses PINTEREST_REFRESH_TOKEN to get a fresh access token from https://api.pinterest.com/v5/oauth/token with grant_type=refresh_token. Cache the token for 25 days. Export a pinterestFetch helper that makes authenticated requests to https://api.pinterest.com/v5/{endpoint} with the Bearer token.

Paste this in Bolt.new chat

lib/pinterest.ts
1// lib/pinterest.ts
2const PINTEREST_API_BASE = 'https://api.pinterest.com/v5';
3const TOKEN_ENDPOINT = 'https://api.pinterest.com/v5/oauth/token';
4
5let cachedToken: { token: string; expiresAt: number } | null = null;
6
7export async function getPinterestToken(): Promise<string> {
8 // Cache token for 25 days (expires at 30 days, refresh early)
9 if (cachedToken && Date.now() < cachedToken.expiresAt) {
10 return cachedToken.token;
11 }
12
13 const appId = process.env.PINTEREST_APP_ID;
14 const appSecret = process.env.PINTEREST_APP_SECRET;
15 const refreshToken = process.env.PINTEREST_REFRESH_TOKEN;
16
17 if (!appId || !appSecret || !refreshToken) {
18 throw new Error(
19 'Missing Pinterest credentials. Set PINTEREST_APP_ID, PINTEREST_APP_SECRET, ' +
20 'and PINTEREST_REFRESH_TOKEN in .env'
21 );
22 }
23
24 const credentials = Buffer.from(`${appId}:${appSecret}`).toString('base64');
25
26 const response = await fetch(TOKEN_ENDPOINT, {
27 method: 'POST',
28 headers: {
29 Authorization: `Basic ${credentials}`,
30 'Content-Type': 'application/x-www-form-urlencoded',
31 },
32 body: new URLSearchParams({
33 grant_type: 'refresh_token',
34 refresh_token: refreshToken,
35 scope: 'ads:read user_accounts:read',
36 }),
37 });
38
39 const data = await response.json() as {
40 access_token?: string;
41 expires_in?: number;
42 refresh_token?: string;
43 error?: string;
44 error_description?: string;
45 };
46
47 if (!response.ok || !data.access_token) {
48 throw new Error(
49 `Pinterest token refresh failed: ${data.error_description || data.error || response.status}`
50 );
51 }
52
53 cachedToken = {
54 token: data.access_token,
55 // Cache for 25 days (token valid 30 days, refresh 5 days early)
56 expiresAt: Date.now() + 25 * 24 * 60 * 60 * 1000,
57 };
58
59 return cachedToken.token;
60}
61
62export async function pinterestFetch<T = unknown>(
63 endpoint: string,
64 options: {
65 params?: Record<string, string>;
66 method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
67 body?: Record<string, unknown>;
68 } = {}
69): Promise<T> {
70 const token = await getPinterestToken();
71
72 const url = new URL(`${PINTEREST_API_BASE}/${endpoint}`);
73 if (options.params) {
74 Object.entries(options.params).forEach(([k, v]) => url.searchParams.set(k, v));
75 }
76
77 const response = await fetch(url.toString(), {
78 method: options.method || 'GET',
79 headers: {
80 Authorization: `Bearer ${token}`,
81 'Content-Type': 'application/json',
82 },
83 body: options.body ? JSON.stringify(options.body) : undefined,
84 });
85
86 if (!response.ok) {
87 const err = await response.json().catch(() => ({})) as { message?: string };
88 throw new Error(`Pinterest API ${response.status}: ${err.message || response.statusText}`);
89 }
90
91 return response.json() as Promise<T>;
92}

Pro tip: Pinterest refresh tokens reset their 1-year expiry each time they are used to obtain a new access token. A dashboard actively fetching data daily will have a refresh token that effectively never expires. If the dashboard is unused for a full year, users need to re-authorize through the OAuth flow.

Expected result: The Pinterest API helper is configured. The getPinterestToken function successfully exchanges the refresh token for an access token. A test call to /v5/user_account should return the authenticated account details.

2

Complete the OAuth Authorization Flow to Get Initial Tokens

Pinterest's OAuth 2.0 requires an initial authorization flow where your app redirects the user to Pinterest's consent screen, the user approves the requested permissions, and Pinterest redirects back to your app with an authorization code that you exchange for access and refresh tokens. The authorization URL format is: `https://www.pinterest.com/oauth/?client_id={APP_ID}&redirect_uri={REDIRECT_URI}&response_type=code&scope=ads:read,user_accounts:read`. Build this URL with your App ID and a redirect URI. For the redirect URI, Pinterest accepts any valid HTTPS URL registered in your app settings. For Bolt's WebContainer preview, the redirect URI constraint applies — Pinterest cannot redirect to the WebContainer's dynamic URLs. There are two practical approaches for getting the initial tokens without deploying: Option 1 — Use a temporary redirect service: Register `https://your-app.netlify.app/api/pinterest/auth/callback` as your redirect URI but deploy a minimal version of the auth callback before completing the authorization. This requires a brief initial deployment. Option 2 — Manual token exchange: Use a tool like the Pinterest API documentation's Try It feature or Postman to complete the OAuth flow manually. Once you have the authorization code from Pinterest's redirect, exchange it for tokens using curl or Postman, then copy the tokens directly to your .env file. This avoids writing the callback route for development purposes. The token exchange endpoint is `POST https://api.pinterest.com/v5/oauth/token` with a Basic Auth header (Base64-encoded `APP_ID:APP_SECRET`), Content-Type `application/x-www-form-urlencoded`, and a body containing `grant_type=authorization_code`, `code={AUTH_CODE}`, and `redirect_uri={SAME_URI_USED_IN_STEP_1}`. The response includes both `access_token` and `refresh_token` — save both to your .env immediately.

Bolt.new Prompt

Create a Next.js API route at app/api/pinterest/auth/route.ts that builds a Pinterest OAuth authorization URL from PINTEREST_APP_ID and redirects to it. Also create app/api/pinterest/auth/callback/route.ts that receives the code from Pinterest's redirect, exchanges it for tokens by calling https://api.pinterest.com/v5/oauth/token with Basic Auth, and returns { accessToken, refreshToken, message: 'Copy these to your .env file' }.

Paste this in Bolt.new chat

app/api/pinterest/auth/callback/route.ts
1// app/api/pinterest/auth/callback/route.ts
2import { NextResponse } from 'next/server';
3
4export async function GET(request: Request) {
5 const { searchParams } = new URL(request.url);
6 const code = searchParams.get('code');
7 const error = searchParams.get('error');
8
9 if (error) {
10 return NextResponse.json({ error, description: searchParams.get('error_description') }, { status: 400 });
11 }
12
13 if (!code) {
14 return NextResponse.json({ error: 'No authorization code received from Pinterest' }, { status: 400 });
15 }
16
17 const appId = process.env.PINTEREST_APP_ID;
18 const appSecret = process.env.PINTEREST_APP_SECRET;
19 const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/pinterest/auth/callback`;
20
21 if (!appId || !appSecret) {
22 return NextResponse.json({ error: 'Pinterest app credentials not configured' }, { status: 500 });
23 }
24
25 const credentials = Buffer.from(`${appId}:${appSecret}`).toString('base64');
26
27 const response = await fetch('https://api.pinterest.com/v5/oauth/token', {
28 method: 'POST',
29 headers: {
30 Authorization: `Basic ${credentials}`,
31 'Content-Type': 'application/x-www-form-urlencoded',
32 },
33 body: new URLSearchParams({
34 grant_type: 'authorization_code',
35 code,
36 redirect_uri: redirectUri,
37 }),
38 });
39
40 const data = await response.json() as {
41 access_token?: string;
42 refresh_token?: string;
43 expires_in?: number;
44 error?: string;
45 };
46
47 if (!response.ok || !data.access_token) {
48 return NextResponse.json({ error: data.error || 'Token exchange failed' }, { status: 500 });
49 }
50
51 return NextResponse.json({
52 accessToken: data.access_token,
53 refreshToken: data.refresh_token,
54 expiresIn: data.expires_in,
55 message: 'Copy PINTEREST_ACCESS_TOKEN and PINTEREST_REFRESH_TOKEN to your .env file',
56 envLines: [
57 `PINTEREST_ACCESS_TOKEN=${data.access_token}`,
58 `PINTEREST_REFRESH_TOKEN=${data.refresh_token}`,
59 ],
60 });
61}

Pro tip: After copying the refresh token to your .env file, delete or protect the auth callback route in production. Anyone with access to your deployed callback URL can complete an OAuth flow against your app. The callback is only needed for the initial setup — once you have the refresh token, the callback serves no further purpose.

Expected result: The auth callback route returns access and refresh tokens after Pinterest redirects to it with an authorization code. Copy the PINTEREST_ACCESS_TOKEN and PINTEREST_REFRESH_TOKEN values to your .env file to enable all subsequent API calls.

3

Fetch Campaign Performance Analytics

Pinterest's Marketing API v5 provides campaign analytics through the `/ad_accounts/{ad_account_id}/campaigns/analytics` endpoint. This endpoint returns performance data for multiple campaigns in a single request — more efficient than fetching analytics per campaign. The analytics endpoint accepts these key parameters: `ad_account_id` (your Pinterest Ads account ID), `start_date` and `end_date` in YYYY-MM-DD format, `columns` (a comma-separated list of metric names), `granularity` (TOTAL, DAY, HOUR, WEEK, or MONTH), and optionally `campaign_ids` to filter specific campaigns. The columns available include Pinterest-specific metrics alongside standard ad metrics. Standard metrics: `SPEND_IN_DOLLAR`, `IMPRESSION_1`, `CLICK_2` (outbound clicks), `ENGAGEMENT_2` (total engagements). Pinterest-unique metrics: `SAVE` (pin saves — the number of times users saved your promoted pin to a board), `SAVE_RATE`, `OUTBOUND_CLICK` (clicks that go to your website), `VIDEO_MRC_VIEW` (for video pins), `VIDEO_P100_COMPLETE` (100% video completions), and `CHECKOUT_ROAS` (for shopping campaigns). The SAVE metric deserves special attention in your dashboard design. A high save rate indicates your promoted pin content resonates strongly with the visual discovery mindset — users want to reference it later. Pins with high save rates often continue generating organic impressions long after the paid campaign ends, through what Pinterest calls 'organic tail' — the ongoing reach from users who discover the saved pin in boards. For a complete campaign list, first call `GET /ad_accounts/{id}/campaigns` to get campaign IDs and metadata (name, status, objective, budget), then call the analytics endpoint with those campaign IDs for performance metrics. Combine them by campaign ID to build the full dashboard dataset.

Bolt.new Prompt

Create a Next.js API route at app/api/pinterest/campaigns/route.ts. Fetch campaigns from Pinterest API at /v5/ad_accounts/{PINTEREST_AD_ACCOUNT_ID}/campaigns. Then fetch analytics at /v5/ad_accounts/{PINTEREST_AD_ACCOUNT_ID}/campaigns/analytics with date range from query params, columns=SPEND_IN_DOLLAR,IMPRESSION_1,CLICK_2,SAVE,SAVE_RATE,OUTBOUND_CLICK,CHECKOUT_ROAS, granularity=TOTAL. Join by campaign_id. Return campaigns sorted by spend descending with totals. Use pinterestFetch from lib/pinterest.ts.

Paste this in Bolt.new chat

app/api/pinterest/campaigns/route.ts
1// app/api/pinterest/campaigns/route.ts
2import { NextResponse } from 'next/server';
3import { pinterestFetch } from '@/lib/pinterest';
4
5interface PinterestCampaign {
6 id: string;
7 name: string;
8 status: string;
9 objective_type: string;
10 daily_spend_cap?: number;
11 lifetime_spend_cap?: number;
12 created_time: number;
13}
14
15interface PinterestCampaignAnalytic {
16 campaign_id: string;
17 date_range: { start_date: string; end_date: string };
18 SPEND_IN_DOLLAR?: number;
19 IMPRESSION_1?: number;
20 CLICK_2?: number;
21 SAVE?: number;
22 SAVE_RATE?: number;
23 OUTBOUND_CLICK?: number;
24 CHECKOUT_ROAS?: number;
25}
26
27export async function GET(request: Request) {
28 const { searchParams } = new URL(request.url);
29 const adAccountId = process.env.PINTEREST_AD_ACCOUNT_ID;
30 if (!adAccountId) {
31 return NextResponse.json({ error: 'PINTEREST_AD_ACCOUNT_ID not configured' }, { status: 500 });
32 }
33
34 const endDate = searchParams.get('endDate') || new Date().toISOString().split('T')[0];
35 const startDate = searchParams.get('startDate') ||
36 new Date(Date.now() - 30 * 86400000).toISOString().split('T')[0];
37
38 try {
39 const [campaignsData, analyticsData] = await Promise.all([
40 pinterestFetch<{ items: PinterestCampaign[] }>(
41 `ad_accounts/${adAccountId}/campaigns`,
42 { params: { page_size: '100' } }
43 ),
44 pinterestFetch<PinterestCampaignAnalytic[]>(
45 `ad_accounts/${adAccountId}/campaigns/analytics`,
46 {
47 params: {
48 start_date: startDate,
49 end_date: endDate,
50 columns: [
51 'SPEND_IN_DOLLAR',
52 'IMPRESSION_1',
53 'CLICK_2',
54 'SAVE',
55 'SAVE_RATE',
56 'OUTBOUND_CLICK',
57 'CHECKOUT_ROAS',
58 ].join(','),
59 granularity: 'TOTAL',
60 },
61 }
62 ),
63 ]);
64
65 const analyticsMap = new Map<string, PinterestCampaignAnalytic>();
66 (Array.isArray(analyticsData) ? analyticsData : []).forEach((row) => {
67 analyticsMap.set(row.campaign_id, row);
68 });
69
70 const campaigns = (campaignsData.items || []).map((campaign) => {
71 const metrics = analyticsMap.get(campaign.id);
72 return {
73 id: campaign.id,
74 name: campaign.name,
75 status: campaign.status,
76 objective: campaign.objective_type,
77 dailyBudget: campaign.daily_spend_cap ? campaign.daily_spend_cap / 1_000_000 : null,
78 spend: metrics?.SPEND_IN_DOLLAR ?? 0,
79 impressions: metrics?.IMPRESSION_1 ?? 0,
80 clicks: metrics?.CLICK_2 ?? 0,
81 saves: metrics?.SAVE ?? 0,
82 saveRate: metrics?.SAVE_RATE ?? 0,
83 outboundClicks: metrics?.OUTBOUND_CLICK ?? 0,
84 roas: metrics?.CHECKOUT_ROAS ?? null,
85 };
86 });
87
88 campaigns.sort((a, b) => b.spend - a.spend);
89
90 const totals = campaigns.reduce(
91 (acc, c) => ({
92 spend: acc.spend + c.spend,
93 impressions: acc.impressions + c.impressions,
94 clicks: acc.clicks + c.clicks,
95 saves: acc.saves + c.saves,
96 }),
97 { spend: 0, impressions: 0, clicks: 0, saves: 0 }
98 );
99
100 return NextResponse.json({ campaigns, totals, dateRange: { startDate, endDate } });
101 } catch (err) {
102 const message = err instanceof Error ? err.message : 'Failed to fetch Pinterest campaigns';
103 return NextResponse.json({ error: message }, { status: 500 });
104 }
105}

Pro tip: Pinterest budget values in campaign objects are in microdollars (divide by 1,000,000), but the analytics endpoint returns SPEND_IN_DOLLAR as actual dollars. Make sure you apply the microdollar conversion only to budget fields and not to the analytics spend column — mixing these up results in budget values showing as millions.

Expected result: The campaigns API route returns Pinterest Ads campaign performance data with correctly parsed spend in dollars and Pinterest-specific metrics like saves and save rate. Verify with /api/pinterest/campaigns in the browser.

4

Build the Pinterest Ads Analytics Dashboard

Pinterest Ads dashboards should prominently feature the Save metric and Save Rate — Pinterest's distinctive advantage over other ad platforms. A save represents a user who found your promoted pin compelling enough to archive for future reference, signaling strong purchase intent that may convert days or weeks after the initial impression. Campaigns with high save rates continue generating organic reach long after the paid campaign ends through 'organic tail' exposure. For the dashboard layout, organize it around Pinterest's visual discovery context: a summary header with total spend, total impressions, total saves, and average save rate; a campaign table with sortable columns prioritizing saves and save rate alongside standard metrics; and a chart showing the relationship between spend and saves across campaigns. Pinterest campaign objectives include: AWARENESS (maximize impressions), CONSIDERATION (maximize website traffic and video views), CONVERSION (maximize purchases and leads), and CATALOG_SALES (dynamic shopping ads). Each objective maps to different primary KPIs — save rate matters most for AWARENESS campaigns, outbound click rate for CONSIDERATION, and ROAS for CONVERSION and CATALOG_SALES. The CHECKOUT_ROAS metric only appears for CONVERSION and CATALOG_SALES campaigns with Pinterest conversion tracking (the Pinterest Tag) properly installed. For campaigns without conversion tracking, ROAS will be null. Display 'N/A' for null ROAS values rather than 0 to make clear the data is unavailable rather than showing a 0x return. For color coding in the table: save rate above 1.5% is strong for Pinterest, 0.5-1.5% is average, below 0.5% warrants creative review. ROAS above 3x is strong, 1-3x is acceptable, below 1x needs intervention.

Bolt.new Prompt

Build a Pinterest Ads dashboard page at /pinterest-ads that fetches from /api/pinterest/campaigns with Last 7d / Last 30d / Last 90d date presets. Show: (1) summary cards for total spend, total impressions, total saves, and average save rate percentage; (2) a Recharts bar chart comparing spend by campaign; (3) a sortable table with Name, Status badge (ACTIVE=green, PAUSED=yellow, ARCHIVED=gray), Objective, Spend, Impressions, Saves, Save Rate (color-coded: >1.5%=green, 0.5-1.5%=yellow, <0.5%=red), Outbound Clicks, and ROAS (N/A if null). Include loading skeletons and error state.

Paste this in Bolt.new chat

Pro tip: Pinterest's SAVE_RATE is returned as a decimal (0.0156 for 1.56%) in some API versions. Multiply by 100 before displaying as a percentage. Also note that IMPRESSION_1 is 'served impressions' — the number of times your pin was shown — and IMPRESSION_2 is 'viewable impressions' (at least 50% visible for 1 second). IMPRESSION_1 is the standard metric for most reporting.

Expected result: The Pinterest Ads dashboard renders with campaign data showing saves and save rate prominently. The date range presets correctly reload data. Save Rate values are color-coded by performance tier.

5

Deploy and Add Pinterest Tag for Conversion Tracking

Outbound Pinterest API calls work in Bolt's WebContainer preview for reading campaign analytics. The Pinterest Tag (conversion pixel) and OAuth redirect callback both require deployment to a live domain. Deploy via Settings → Applications → Connect Netlify or click Publish to deploy to Bolt Cloud. Add server-side environment variables: `PINTEREST_APP_ID`, `PINTEREST_APP_SECRET`, `PINTEREST_REFRESH_TOKEN`, and `PINTEREST_AD_ACCOUNT_ID`. Add the client-side `NEXT_PUBLIC_PINTEREST_TAG_ID` for the Pinterest Tag conversion pixel. The Pinterest Tag is a JavaScript snippet that fires conversion events on your site, feeding data back to Pinterest for campaign optimization and ROAS tracking. Without the Tag, CHECKOUT_ROAS shows as N/A and Pinterest cannot optimize toward conversions. Install it similarly to the Quora Pixel: create a component using Next.js Script that loads `https://s.pinimg.com/ct/core.js`, initializes with your Tag ID, and fires PageView on every page load. For conversion events, fire `pintrk('track', 'checkout', { value: 99.00, order_quantity: 1, currency: 'USD', line_items: [] })` after successful purchases, and `pintrk('track', 'lead')` after form submissions. Pinterest's Conversions API (distinct from the Marketing API) provides server-side event tracking — the endpoint is `https://api.pinterest.com/v5/ad_accounts/{ad_account_id}/events` and accepts the same OAuth Bearer token from your Marketing API app. For registering a production redirect URI: in your Pinterest developer app settings, add your deployed domain's callback URL (`https://your-app.netlify.app/api/pinterest/auth/callback`). This allows future re-authorization without changing app settings.

Bolt.new Prompt

Create a components/PinterestTag.tsx component using Next.js Script that initializes the Pinterest Tag with NEXT_PUBLIC_PINTEREST_TAG_ID from process.env and fires a PageView event. Add it to root layout.tsx. Create a hooks/usePinterestTrack.ts hook that wraps pintrk() for type-safe conversion tracking. Add checkout tracking call in the purchase success handler with value, order_quantity, and currency fields.

Paste this in Bolt.new chat

components/PinterestTag.tsx
1// components/PinterestTag.tsx
2'use client';
3
4import Script from 'next/script';
5
6declare global {
7 interface Window {
8 pintrk?: (...args: unknown[]) => void;
9 }
10}
11
12export function PinterestTag() {
13 const tagId = process.env.NEXT_PUBLIC_PINTEREST_TAG_ID;
14 if (!tagId) return null;
15
16 return (
17 <>
18 <Script
19 id="pinterest-tag"
20 strategy="afterInteractive"
21 dangerouslySetInnerHTML={{
22 __html: `
23 !function(e){
24 if(!window.pintrk){
25 window.pintrk = function () {
26 window.pintrk.queue.push(Array.prototype.slice.call(arguments))
27 };
28 var n=window.pintrk;
29 n.version="3.0",n.queue=[];
30 var t=document.createElement("script");
31 t.async=!0,t.src=e;
32 var r=document.getElementsByTagName("script")[0];
33 r.parentNode.insertBefore(t,r)
34 }
35 }("https://s.pinimg.com/ct/core.js");
36 pintrk('load', '${tagId}', { em: '' });
37 pintrk('page');
38 `,
39 }}
40 />
41 <noscript>
42 <img
43 height="1"
44 width="1"
45 style={{ display: 'none' }}
46 alt=""
47 src={`https://ct.pinterest.com/v3/?event=init&tid=${tagId}&pd[em]=&noscript=1`}
48 />
49 </noscript>
50 </>
51 );
52}

Pro tip: Pinterest's Tag verification tool is at the Pinterest Ads Manager under Conversions → Pinterest Tag. Use it to verify the tag is firing on your deployed site. Pinterest also offers a browser extension called 'Pinterest Tag Helper' for real-time event debugging, similar to the Quora Pixel Helper.

Expected result: After deploying, the Pinterest Tag fires PageView events on every page of the live site. Checkout events appear in Pinterest Ads Manager → Conversions → Event History within a few hours. The CHECKOUT_ROAS metric starts populating in campaign analytics once conversions are tracked.

Common use cases

Pinterest Campaign Performance Dashboard

Build a performance dashboard showing all active Pinterest Ads campaigns with key metrics: spend, impressions, clicks, saves (pin saves to boards), save rate, and conversions. Surface the save rate metric prominently since saves represent strong purchase intent on Pinterest and are a leading indicator of future conversions that platforms like Facebook do not provide.

Bolt.new Prompt

Build a Pinterest Ads analytics dashboard. Create a Next.js API route at /api/pinterest/campaigns that uses Pinterest Marketing API v5 with PINTEREST_ACCESS_TOKEN from process.env to fetch all campaigns in a business account. For each campaign, get analytics (spend, impressions, clicks, saves, total_conversions) for a date range from query params. Use ad_account_id from PINTEREST_AD_ACCOUNT_ID env var. Build a React dashboard with a summary header of total spend, total impressions, total saves, and save rate percentage. Show a sortable campaign table with columns for Name, Status, Spend, Impressions, Clicks, Saves, Save Rate, and Conversions.

Copy this prompt to try it in Bolt.new

Promoted Pin Creative Performance Analyzer

Analyze promoted pin performance at the creative level to identify which images, titles, and descriptions resonate most with Pinterest's audience. Compare save rate, click rate, and outbound click rate across pin variants to guide creative optimization. Pinterest's visual format means creative quality has an outsized impact on performance compared to text-based ad platforms.

Bolt.new Prompt

Create a promoted pin analyzer. Build a Next.js API route at /api/pinterest/pins that fetches all active promoted pins in a Pinterest Ads ad account with their analytics: impressions, clicks, saves, outbound_clicks, pin_click_rate, save_rate, total_conversions. Include the pin's thumbnail image URL and title. Build a React grid showing each pin's thumbnail, headline metrics, and performance tier badge (Top 20%, Average, Needs Review) based on save rate percentile. Sort by save rate descending. Use PINTEREST_ACCESS_TOKEN and PINTEREST_AD_ACCOUNT_ID from process.env.

Copy this prompt to try it in Bolt.new

Pinterest Shopping Campaign and Catalog Tracker

Track performance of Pinterest Shopping ads driven by a product catalog, showing which products receive the most impressions, clicks, and add-to-cart events. For e-commerce brands using Pinterest Shopping, analyze which product categories and price points perform best to inform catalog optimization and bidding strategy.

Bolt.new Prompt

Build a Pinterest Shopping performance tracker. Create a Next.js API route at /api/pinterest/shopping that fetches Shopping campaign performance from Pinterest Marketing API, including product group metrics and catalog impressions. Accept date range from query params. Build a React table showing product groups with impressions, clicks, spend, ROAS, and checkout conversions. Include a chart showing spend vs. ROAS correlation across product groups. Color-code ROAS values: green above 3x, yellow 1-3x, red below 1x. Store credentials in process.env.

Copy this prompt to try it in Bolt.new

Troubleshooting

Token refresh fails with 'invalid_grant' error from Pinterest OAuth

Cause: The refresh token has expired (1 year of inactivity) or was used with a different App Secret than the one currently configured. Pinterest refresh tokens are bound to the app that issued them.

Solution: If the token expired, complete the OAuth authorization flow again to get a new refresh token. If the App Secret changed (you regenerated it in the developer portal), you must re-authorize since the old refresh token is bound to the old secret. Store the refresh token alongside the App ID and Secret that were used to generate it.

Analytics endpoint returns empty array even though campaigns have spend

Cause: The campaign analytics endpoint requires campaigns to have delivery data in the requested date range. Recently created campaigns or campaigns paused before the date range started will not appear in analytics even though the campaign list endpoint returns them.

Solution: Verify the date range includes periods when the campaigns were actively running. Check campaign dates in the campaigns list response. For recently activated campaigns, allow 24-48 hours for analytics data to populate. The analytics endpoint silently omits campaigns with no data rather than returning zero rows for them.

typescript
1// Handle campaigns missing from analytics:
2const campaigns = (campaignsData.items || []).map((campaign) => {
3 const metrics = analyticsMap.get(campaign.id);
4 // metrics will be undefined for campaigns with no data in range
5 return {
6 ...campaign,
7 spend: metrics?.SPEND_IN_DOLLAR ?? 0, // 0 if no data, not an error
8 hasData: !!metrics,
9 };
10});

SAVE_RATE returns as a decimal like 0.0234 instead of a percentage

Cause: Pinterest returns save rate as a decimal fraction (0.0234 = 2.34%). This format differs from platforms like Facebook that return CTR as a percentage string.

Solution: Multiply the SAVE_RATE value by 100 before displaying it. Store the raw decimal from the API and only multiply at display time, so the stored value can be used in calculations without needing to convert back.

typescript
1// In the API route, convert save rate to percentage:
2saveRate: metrics?.SAVE_RATE ? metrics.SAVE_RATE * 100 : 0,
3// In the React component, display with toFixed(2):
4`${campaign.saveRate.toFixed(2)}%`

OAuth callback URL does not match — Pinterest rejects the authorization code exchange

Cause: The redirect_uri in the token exchange request must exactly match the redirect_uri registered in the Pinterest developer app settings and used in the initial authorization URL. Any difference — trailing slash, http vs https, subdomain — causes Pinterest to reject the exchange.

Solution: Verify the redirect_uri in your token exchange request exactly matches what was registered in the Pinterest developer app settings. Update your app settings at developers.pinterest.com to match your deployed URL. Ensure the same redirect_uri was used in both the initial authorization URL and the token exchange request.

Best practices

  • Prominently feature the Save metric and Save Rate in your Pinterest dashboard — they are Pinterest's most distinctive performance signals and often better predict purchase intent than click-through rate alone.
  • Cache Pinterest access tokens in a server-side module-level variable for up to 25 days since Pinterest tokens last 30 days — this eliminates redundant token refresh calls on every API request.
  • Install the Pinterest Tag on your deployed app for conversion tracking — without it, CHECKOUT_ROAS shows as N/A and Pinterest cannot optimize toward conversions, significantly limiting campaign performance.
  • Use the Pinterest Conversions API alongside the browser Tag for purchase events to capture conversions from users with ad blockers or Safari's ITP restrictions, same deduplication approach as other platforms.
  • Display ROAS as 'N/A' rather than '0' when the value is null — null ROAS means conversion tracking is not set up or the campaign objective does not support ROAS tracking, not a zero return on ad spend.
  • Request only the analytics columns you display in your dashboard rather than all available columns — Pinterest's analytics API responds faster with fewer columns and you reduce the risk of encountering null fields for metrics not supported by certain campaign objectives.
  • Store Pinterest budget values from the campaigns endpoint as microdollars internally and convert to dollars only for display — this prevents double-conversion bugs since the analytics SPEND_IN_DOLLAR is already in dollars.
  • Test your deployed Pinterest Tag using the Pinterest Tag Helper browser extension and the verification tool in Pinterest Ads Manager — the tag does not fire reliably in Bolt's WebContainer preview due to the preview environment's security policy.

Alternatives

Frequently asked questions

Does Bolt.new have a native Pinterest Ads integration?

No — Bolt.new does not have a native Pinterest Ads connector. The integration requires building Next.js API routes that call Pinterest's Marketing API v5 with OAuth 2.0 authentication. Bolt's AI can generate this boilerplate from a descriptive prompt, which handles authentication, API calls, and the dashboard UI without manual coding.

Can I test the Pinterest Ads dashboard in Bolt's WebContainer without deploying?

Yes, for the API analytics portion — all outbound calls to Pinterest's Marketing API work in Bolt's WebContainer since they are HTTPS requests. You can read campaign data and build the dashboard in the preview. The two features requiring deployment are the OAuth redirect callback (only needed for the initial token setup) and the Pinterest Tag conversion pixel, which needs a live domain to fire correctly.

What makes Pinterest Ads different from other social advertising platforms?

Pinterest users arrive with explicit discovery and planning intent — they are actively searching for ideas and products to buy or make. This makes Pinterest's Save metric unique: when users save your promoted pin to a board, they are signaling intent to reference it for a future purchase or project. Saved pins also generate ongoing organic impressions after the campaign ends, creating an 'organic tail' of reach that platforms like Facebook and Google do not provide.

Why is my CHECKOUT_ROAS showing as null or N/A?

CHECKOUT_ROAS requires the Pinterest Tag to be installed on your site with checkout events firing, and the campaign objective must be set to CONVERSION or CATALOG_SALES. Awareness and Consideration campaigns do not track ROAS since their objective is not driving purchases. Install the Pinterest Tag, fire checkout events with value parameters after successful purchases, and allow 24-48 hours for ROAS data to appear in campaign analytics.

How do Pinterest access tokens and refresh tokens work?

Pinterest access tokens expire after 30 days. Refresh tokens expire after 1 year but reset their expiry each time they are used. A dashboard that makes daily API calls effectively never has an expiring refresh token. Store the refresh token in your .env file and use it to obtain a fresh access token automatically in your API helper. The access token should be cached in-memory for 25 days to avoid unnecessary refresh calls.

Do I need to go through an approval process to use Pinterest's Marketing API?

Pinterest Marketing API access requires creating a developer app and having it approved, which typically takes 24-48 hours for legitimate business use cases. Unlike some platforms, there is no lengthy manual review or partner program requirement for basic campaign reading and analytics access. The ads:read and user_accounts:read scopes needed for a dashboard are approved as part of standard developer app review.

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.