Ads
Run Promoted Pins on a pinterestads account, from a new Pin or one you already published, with targeting, customer lists, the Pinterest tag and conversions.
A Pinterest ad is a Promoted Pin. You can create a fresh Pin and promote it in one request, or put budget behind a Pin that is already live and keep its saves and comments. Both run on a pinterestads connection, and the ad endpoints refer to the network as pinterest.
| Connection platform | pinterestads |
platform on ad endpoints |
pinterest |
| Structure | Campaign, ad group, Promoted Pin, all created by one call |
| Goals | engagement, traffic, awareness, video_views |
| Minimum budget | 5 in the ad account currency, daily or lifetime |
| Changes after launch | Status and budget |
| Audiences | Customer lists, matched on email |
| Measurement | Pinterest tag and Conversions API |
#Connect
Pinterest uses one token for posting and for ads. If the profile already has a Pinterest account connected, the ads connection reuses its token and no consent screen appears.
curl "https://api.pozy.ai/v1/connect/pinterest/ads?profileId=66b2e19d8c3f5a7e9d0b1c2a" \
-H "Authorization: Bearer $POZY_API_KEY"const res = await fetch("https://api.pozy.ai/v1/connect/pinterest/ads?profileId=66b2e19d8c3f5a7e9d0b1c2a", {
headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.alreadyConnected ? data.accountId : data.authUrl);import os, requests
resp = requests.get(
"https://api.pozy.ai/v1/connect/pinterest/ads",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
params={"profileId": "66b2e19d8c3f5a7e9d0b1c2a"},
)
print(resp.json()){
"alreadyConnected": true,
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platform": "pinterest",
"username": "@acmehome",
"displayName": "Acme Home"
}accountId is the pinterestads connection. Every example below uses it.
- No Pinterest account in the profile yet. The response carries
authUrlandstate. Send the person toauthUrl; the consent screen asks forads:readandads:writealong with the posting scopes. - Connected before ads existed. A Pinterest account connected without the ads scopes goes through consent once more.
- Token can no longer reach ad accounts. Calls return
400asking for a reconnect. Reconnect the Pinterest posting account and the ads connection picks up the new token.
Pinterest does not let Pozy discover ad accounts at connect time, so adAccountId and adAccountIds on the connect call have no effect. List the ad accounts with GET /v1/ads/accounts?accountId=... and pass adAccountId on each request instead.
#Create a Promoted Pin
POST /v1/ads/create creates the Pin, then the campaign, ad group and ad around it. Because the Pin is built from the request, send headline, body, imageUrl and linkUrl together.
curl -X POST https://api.pozy.ai/v1/ads/create \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: spring-decor-2027-01" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "549123456789",
"name": "Spring home decor",
"goal": "traffic",
"budgetAmount": 30,
"budgetType": "daily",
"headline": "Freshen up your space",
"body": "New spring pieces, in stock now.",
"imageUrl": "https://cdn.example.com/spring-decor.jpg",
"linkUrl": "https://example.com/spring",
"boardId": "1029384756473829101",
"countries": ["US", "CA"],
"ageMin": 25,
"ageMax": 44
}'const res = await fetch("https://api.pozy.ai/v1/ads/create", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Idempotency-Key": "spring-decor-2027-01",
"Content-Type": "application/json",
},
body: JSON.stringify({
accountId: "66b2e19d8c3f5a7e9d0b1c2d",
adAccountId: "549123456789",
name: "Spring home decor",
goal: "traffic",
budgetAmount: 30,
budgetType: "daily",
headline: "Freshen up your space",
body: "New spring pieces, in stock now.",
imageUrl: "https://cdn.example.com/spring-decor.jpg",
linkUrl: "https://example.com/spring",
boardId: "1029384756473829101",
countries: ["US", "CA"],
ageMin: 25,
ageMax: 44,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.ad._id, data.ad.status);resp = requests.post(
"https://api.pozy.ai/v1/ads/create",
headers={
"Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
"Idempotency-Key": "spring-decor-2027-01",
},
json={
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "549123456789",
"name": "Spring home decor",
"goal": "traffic",
"budgetAmount": 30,
"budgetType": "daily",
"headline": "Freshen up your space",
"body": "New spring pieces, in stock now.",
"imageUrl": "https://cdn.example.com/spring-decor.jpg",
"linkUrl": "https://example.com/spring",
"boardId": "1029384756473829101",
"countries": ["US", "CA"],
"ageMin": 25,
"ageMax": 44,
},
)
print(resp.json()["ad"]["status"]){
"ad": {
"_id": "66f0a1b2c3d4e5f6a7b8c9d0",
"name": "Spring home decor",
"platform": "pinterest",
"status": "pending_review",
"adType": "standalone",
"goal": "traffic",
"budget": { "amount": 30, "type": "daily" },
"platformCampaignId": "626746010001",
"platformAdSetId": "2680069880002",
"platformAdId": "687195120340"
},
"message": "Ad created"
}platformAdSetId is the Pinterest ad group. The ad starts in pending_review and moves to active once Pinterest approves it. Subscribe to ad.review.approved and ad.review.rejected instead of polling.
boardId decides which board the new Pin lands on. Without it, Pinterest Ads puts the Pin on a separate ads board created the first time you launch. Pass a board you own so the Pin sits where your audience expects it.
#Copy and creative
| Field | Rule |
|---|---|
headline |
Required. Up to 100 characters. |
body |
Up to 500 characters. Send it with the headline. |
imageUrl |
Required. A public URL that returns the image bytes. Pozy fetches it. |
linkUrl |
Required. Where a tap on the Pin goes. |
boardId |
Optional. The board for the new Pin. |
To run a video Pin, publish the video to Pinterest first (see Pinterest) and promote it with a boost.
#Budget and schedule
budgetAmountis in whole units of the ad account currency, so30is $30.00 on a USD account. The minimum is 5.budgetTypeisdailyorlifetime.startDateandendDateset the run window. Without them the Promoted Pin runs until you pause it.- Some ad accounts require Campaign Budget Optimization. Those need a
lifetimebudget with anendDate, and a create without one returns400namingendDate.
#Promote a Pin you already published
POST /v1/ads/boost keeps the Pin's existing engagement. Pass the Pozy postId of a post published to Pinterest, or the Pinterest Pin id as platformPostId.
curl -X POST https://api.pozy.ai/v1/ads/boost \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: boost-65f1c0a9" \
-d '{
"postId": "65f1c0a9e2b5af0012ab34cd",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "549123456789",
"name": "Boost decor Pin",
"goal": "traffic",
"budget": { "amount": 30, "type": "daily" },
"schedule": { "startDate": "2027-01-04T09:00:00Z", "endDate": "2027-02-04T23:59:00Z" }
}'The same four goals and the 5 minimum apply. A boost is the route for video Pins. More on boosting across networks is in Boost posts.
#Targeting
Targeting is set on the create and cannot change afterwards.
| Field | What it targets |
|---|---|
countries |
ISO country codes |
regions, metros, zips |
Sub-country locations. Send regions or zips, never both (400 mutually_exclusive_fields). |
ageMin, ageMax, gender |
Demographics |
languages |
ISO 639-1 codes. A code Pinterest cannot target returns 400. |
interests |
{ "id": "..." } values from GET /v1/ads/interests |
audienceInclude, audienceExclude |
Pinterest customer list ids |
incomeTier, cities, customLocations and behaviors are not available on Pinterest and return 400. The cross-network field list is in Targeting.
#Customer lists
Create the audience with type: "customer_list", then upload members. Pinterest matches on email only, so a member with only a phone is not matched.
curl -X POST https://api.pozy.ai/v1/ads/audiences \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "adAccountId": "549123456789", "type": "customer_list", "name": "Repeat buyers" }'
curl -X POST https://api.pozy.ai/v1/ads/audiences/66e5b2c3d4f5a6b7c8d9e0f1/users \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "users": [{ "email": "jane@example.com" }, { "email": "sam@example.com" }] }'- Send plain emails. Pozy hashes each one with SHA-256 before it leaves.
- One upload takes up to 10,000 users.
- Pinterest needs members to create a list, so the audience stays
pendingwith no Pinterest id until the first upload. That upload creates it on Pinterest.
#Manage a running campaign
After launch you can change delivery and money, nothing else.
| Change | Call |
|---|---|
| Pause or resume the campaign | PUT /v1/ads/campaigns/{campaignId}/status with { "status": "paused", "platform": "pinterest" } |
| Pause or resume an ad group or a Promoted Pin | PUT /v1/ads/ad-sets/{adSetId}/status or PUT /v1/ads/{adId}/status |
| Change the budget | PUT /v1/ads/{adId} with budget: { amount, type }. It is written to the Pin's ad group. |
| Rename the ad group | PUT /v1/ads/ad-sets/{adSetId} with name |
| Stop for good | DELETE /v1/ads/{adId}, /v1/ads/ad-sets/{adSetId} or /v1/ads/campaigns/{campaignId} |
Each level has its own switch. Resuming a campaign only restarts the ad groups and Pins whose own switch is on. Pinterest does not report a campaign's switch back, so platformCampaignStatus is always null on Pinterest.
Pinterest has no hard delete. Deleting archives the object and marks Pozy's copy cancelled, which keeps its spend in your reports. If you delete a campaign that still has ad groups Pozy does not know about, the call returns 409 invalid_resource_state and nothing changes; delete the ad groups first or sync the ad account and try again.
Sending targeting or creative to PUT /v1/ads/{adId} returns 501 feature_not_available. Create a new ad instead. Campaign duplication is not available on Pinterest either.
#Measure results
GET /v1/ads/{adId}/analytics returns spend, impressions, clicks, CTR, CPC and CPM for a date range, as a summary and a daily series. GET /v1/ads/tree rolls the same numbers up per campaign and ad group.
curl "https://api.pozy.ai/v1/ads/66f0a1b2c3d4e5f6a7b8c9d0/analytics?fromDate=2027-01-04&toDate=2027-01-11" \
-H "Authorization: Bearer $POZY_API_KEY"reachreads0on Pinterest because it is not synced. Use impressions.- On connect, Pozy imports the last 90 days of metrics. Days before that have no data.
- Demographic
breakdownsare not available for Pinterest ads.
Reporting covers the cross-network metrics.
#Pinterest tag and conversions
Pinterest measures site activity with the Pinterest tag, managed through the tracking tags endpoints on the pinterestads connection. A few Pinterest rules to plan around:
- Create a tag with
POST /v1/accounts/{accountId}/tracking-tags. Enhanced match (automaticMatchingFields) can only be set at creation, because Pinterest cannot edit a tag later. - Creating a tag is not idempotent and Pinterest has no tag delete. After a timeout, list the tags before you try again.
- Custom conversion events belong to the ad account: up to 15, named with 1 to 100 letters, digits,
_or-. - Install the tag on a connected Shopify, WordPress, Wix or Webflow site with one call. See Pixels.
Server-side events go to POST /v1/ads/conversions with the numeric ad account id as destinationId, because Pinterest attributes to the ad account rather than to a tag. The connected Pinterest user needs a Business Access role on that ad account. Details, hashing and dedupe are in Conversions.
#Errors you will see
| Status and code | Cause | Fix |
|---|---|---|
400 missing_required_field |
headline, body, imageUrl, linkUrl or goal missing |
Send all of them. Pinterest builds the Pin from them. |
400 invalid_field_value |
A goal outside the four, a headline over 100 or body over 500 characters | Use a supported goal and trim the copy. |
400 mutually_exclusive_fields |
regions and zips together |
Send one of them. |
400 naming endDate |
The ad account requires Campaign Budget Optimization | Send a lifetime budget with endDate. |
403 |
The Pinterest user lacks Business Access on the ad account (tags, conversions) | Grant Admin, Analyst, Audience or Campaign access in Pinterest. |
403 ads_allowance_exceeded |
No payment method and the workspace reached its free live-ads allowance | Add a payment method. |
501 feature_not_available |
Targeting or creative edits after launch | Create a new ad. |
502 platform_api_error |
Pinterest rejected the request | Read platformError in the body for Pinterest's own message. |
#Endpoints
#Webhook events
| Event | When it fires |
|---|---|
account.ads.initial_sync_completed |
Ads initial sync completed. |
account.ads.sync_failed |
Ads sync failed. |
account.ads.sync_recovered |
Ads sync recovered. |
ad.status_changed |
Ad status changed. |
ad.review.approved |
Ad review approved. |
ad.review.rejected |
Ad review rejected. |