Pinterest Ads
Create Promoted Pin campaigns, promote Pins you already published, upload customer list audiences and read ad metrics on a pinterestads account.
Create a Promoted Pin campaign with POST /v1/ads/create, or promote a Pin you already published with POST /v1/ads/boost, on a pinterestads account. The ads account reuses the token of the Pinterest posting account in the same profile, so there is no second OAuth flow and no Pinterest developer application to file.
Quick reference
| Property | Value |
|---|---|
| Hierarchy | Campaign > Ad Group > Promoted Pin, created in one call |
Goals (goal) | engagement, traffic, awareness, video_views |
| Headline | Required, 100 characters |
| Body | Required, 500 characters |
| Creative | imageUrl and linkUrl, both required |
| Minimum budget | $5 (budgetAmount, or budget.amount on a boost) |
| Board | boardId, optional |
| Targeting | Country, region, metro, postal code, age, gender, language, interests |
| Audiences | Customer list |
| Analytics | Yes |
| Edits after creation | Status and budget only |
| Pinterest tag | Create (with enhanced match), read, custom events, stats and Shopify / WordPress install (Pinterest tag) |
| Conversions API | Yes, with Event Quality Score (Conversions API) |
| Catalog and shopping ads | Roadmap |
Before you start
You need a connected Pinterest account in the profile (Pinterest) and the Pinterest ad account id, from List ad accounts. Ads run on 2 scopes (scopes):
| Scope | What it enables |
|---|---|
ads:read | Read ad accounts and reporting |
ads:write | Create and manage campaigns, ad groups and Promoted Pins |
New Pinterest connections request both as part of the standard flow. An account connected before Pinterest Ads existed re-consents to these 2 scopes.
Connect
Call GET /v1/connect/pinterest/ads with profileId (Connect ads). Pinterest shares one token between posting and ads, so when the profile already holds an active Pinterest account the pinterestads account inherits that token and no OAuth happens.
curl "https://zernio.com/api/v1/connect/pinterest/ads?profileId=66a1f0c2a4b9d3e8f1a2b3c4" \
-H "Authorization: Bearer $ZERNIO_API_KEY"Response (200):
{
"alreadyConnected": true,
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platform": "pinterest",
"username": "@acmehome",
"displayName": "Acme Home"
}Without an active Pinterest account the response carries authUrl and state instead; send the user to authUrl, as in the connecting accounts guide. A Pinterest account whose stored token cannot reach ad accounts fails with 400 RECONNECT_REQUIRED, which reconnecting the posting account clears. The accountId in the response is the pinterestads account every sample below uses.
Pinterest has no ads discovery, so the adAccountId and adAccountIds scoping parameters are accepted and ignored on this call. Pass the ad account per request instead.
Create a Promoted Pin
Pinterest creates the Pin and promotes it in the same call, so headline, body, imageUrl and linkUrl are all required. boardId chooses the board; without it the Pin lands on a Zernio Ads board that Zernio creates once. startDate and endDate set the run window at the top level of the body, where the boost below takes a schedule object instead; omit them and the Promoted Pin runs until you pause it. An ad account that requires Campaign Budget Optimization needs endDate with a lifetime budget and returns a 400 naming endDate without one.
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: created } = await zernio.adcampaigns.createStandaloneAd({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: '549123456789',
name: 'Spring home decor',
goal: 'traffic',
budgetAmount: 30,
budgetType: 'daily',
headline: 'Freshen up your space',
body: 'Spring drops, limited time.',
imageUrl: 'https://cdn.example.com/spring-decor.jpg',
linkUrl: 'https://example.com/spring-decor',
countries: ['US', 'CA'],
ageMin: 25,
ageMax: 44
}
});
console.log(created.ad.platformAdSetId);Response (201):
{
"ad": {
"_id": "66f0a1b2c3d4e5f6a7b8c9d0",
"name": "Spring home decor",
"platform": "pinterest",
"status": "pending_review",
"adType": "standalone",
"goal": "traffic",
"budget": { "amount": 30, "type": "daily" },
"platformAdId": "687195120340",
"platformCampaignId": "626746010001",
"platformAdSetId": "2680069880002"
},
"message": "Ad created"
}budgetAmount is in whole currency units of the ad account, so 30 is $30.00 on a USD account. Every sample below reuses the zernio and client constructors from this one.
Targeting fields
countries, regions, metros, zips, ageMin, ageMax, gender, languages (ISO 639-1; a code Pinterest cannot target returns 400) and interests (from GET /v1/ads/interests) apply on Pinterest, and audienceInclude and audienceExclude take the Pinterest customer list id of an audience. Pinterest's geo spec takes region codes or postal codes but never both, so sending regions and zips together returns a 400 with code mutually_exclusive_fields. incomeTier is rejected on Pinterest, and cities, customLocations and behaviors are Meta and TikTok only.
Promote an existing Pin
POST /v1/ads/boost promotes a Pin that is already published, keeping its engagement. Pass the Zernio postId (or the Pinterest platformPostId), the account, the ad account, a goal and a budget.
const { data: boosted } = await zernio.adcampaigns.boostPost({
body: {
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' }
}
});
console.log(boosted.ad._id);Response (201):
{
"ad": {
"_id": "66f0a1b2c3d4e5f6a7b8c9d1",
"name": "Boost decor Pin",
"platform": "pinterest",
"status": "pending_review",
"adType": "boost",
"goal": "traffic",
"budget": { "amount": 30, "type": "daily" }
},
"message": "Ad created"
}Boosts are not idempotent. Send an Idempotency-Key header to make a retry replay the original 201 instead of creating a second ad (idempotency).
Customer list audiences
Create the audience with POST /v1/ads/audiences (type: "customer_list"), then upload members with POST /v1/ads/audiences/{audienceId}/users. Pinterest matches on email, ignores phone, and takes at most 10,000 users per request; Zernio hashes every value with SHA-256 before it leaves.
Pinterest needs the member file at creation time, so the audience is provisioned lazily: the create records it with status pending and no platform id, and the first upload creates it on Pinterest.
const { data: audience } = await zernio.adaudiences.createAdAudience({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: '549123456789',
type: 'customer_list',
name: 'Repeat buyers'
}
});
const { data: upload } = await zernio.adaudiences.addUsersToAdAudience({
path: { audienceId: audience.audience.id },
body: { users: [{ email: 'jane@example.com' }, { email: 'sam@example.com' }] }
});
console.log(upload.numReceived);Response (201):
{
"audience": { "id": "66e5b2c3d4f5a6b7c8d9e0f1", "name": "Repeat buyers", "type": "customer_list", "platform": "pinterest", "status": "pending" },
"message": "Audience created"
}Response (200):
{ "message": "Users added", "numReceived": 2, "numInvalid": 0 }Media requirements
| Type | Formats | Max size | Notes |
|---|---|---|---|
| Standard Pin | JPEG, PNG | 20 MB | 1000 x 1500 px (2:3) recommended |
| Video Pin | MP4, MOV, M4V | 2 GB | 4 seconds to 15 minutes, 9:16 or 1:1 |
Creatives are fetched from the URL you pass, which must be public and return the media bytes (media uploads).
Analytics
Call GET /v1/ads/{adId}/analytics with the ad's _id for spend, impressions, clicks, CTR, CPC and CPM over a date range (Get ad analytics). Pinterest reports saves and closeups alongside those standard metrics.
curl "https://zernio.com/api/v1/ads/66f0a1b2c3d4e5f6a7b8c9d0/analytics?fromDate=2027-01-04&toDate=2027-01-11" \
-H "Authorization: Bearer $ZERNIO_API_KEY"Response (200), trimmed:
{
"ad": { "id": "66f0a1b2c3d4e5f6a7b8c9d0", "name": "Spring home decor", "platform": "pinterest", "status": "active", "currency": "USD" },
"analytics": {
"summary": { "spend": 186.5, "impressions": 74300, "clicks": 1240, "ctr": 1.67, "cpc": 0.15, "cpm": 2.51 },
"daily": [ { "date": "2027-01-04", "spend": 27.9, "impressions": 10600, "clicks": 181 } ]
}
}Pinterest reports reach as 0, because Zernio does not sync it, and the demographic breakdowns parameter is Meta and TikTok only. GET /v1/ads/tree rolls the same metrics up per campaign and ad group (Get campaign tree).
Pinterest tag
Create and read Pinterest tags and manage their custom events through the platform-neutral tracking tags API on your pinterestads account, check when a tag last fired and its Event Quality Score, and install it on a connected Shopify store or WordPress site.
| Field | Value |
|---|---|
tagId | The numeric Pinterest tag id. It is also what the on-site code loads, so siteTagId equals id. |
adAccountId | The numeric ad account id. A tag belongs to one ad account; without adAccountId, per-tag calls find the owner across every ad account the connection can read. |
kind is tag. GET .../tracking-tags lists the tags of every readable ad account (one account refusing access does not hide the others), and GET .../tracking-tags/{tagId} adds the tag's code (Pinterest's base code), lastFiredTime and installed.
const { data } = await zernio.trackingtags.createTrackingTag({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
body: { adAccountId: '549755885175', name: 'Storefront tag' }
});Response (201):
{
"platform": "pinterestads",
"tag": {
"id": "2612345678901",
"siteTagId": "2612345678901",
"name": "Storefront tag",
"platform": "pinterestads",
"kind": "tag",
"status": "active",
"lastFiredTime": null,
"installed": false,
"ownerAdAccountId": "549755885175",
"code": "<script type=\"text/javascript\">...pintrk('load', '2612345678901');...</script>"
}
}The create takes a name and, optionally, automaticMatchingFields to switch on automatic enhanced match for those fields (em, ph, fn, ln, ct, st, zp, country, external_id, ...), which is the only time Zernio can set it: Pinterest cannot edit a tag afterwards. The create is not idempotent: a retry creates a second tag, and Pinterest has no delete, so never retry it blindly.
| Operation | Behavior |
|---|---|
Stats (GET .../stats) | No time range (startTime/endTime answer 400). Rows: the tag (lastFiredTime, status, enhancedMatchStatus), the events Pinterest has seen on it (page_visit and ocpm_eligible sources, with the matching siteEvent), and the ad account's Event Quality Score for the last 1 and 14 days. An account Pinterest cannot score yet reports why in its row. |
| Update | 501: API v5 only creates, lists and reads conversion tags (PATCH and PUT answer 405); rename a tag or change enhanced match in Pinterest Ads Manager. |
| Share | 501: a tag belongs to one ad account; use Pinterest Business Access. |
Conversion events (.../events) | Pinterest's advertiser defined events: a custom event name the site sends, mapped to a standard type. List, create (name plus type or siteEvent), update (only the type: the name is the event's id, so it cannot be renamed) and delete (Pinterest stops tracking it, state: "disabled"). They belong to the ad account, so every tag of that account lists the same ones. At most 15 per ad account, names of 1 to 100 letters, digits, _ or -, case-insensitive. type is one of Pinterest's optimizable types (CHECKOUT, ADD_TO_CART, LEAD, SIGNUP, PAGE_VISIT, ...). |
| Diagnostics, users and partners | 501. |
| Delete | Not offered: DELETE on a conversion tag answers 405 at Pinterest. |
| Install | Shopify and WordPress. WordPress gets Pinterest's base code with pintrk('page') and a pagevisit event (official fallback: Pinterest for WooCommerce, WooCommerce stores only). Shopify sends every storefront event to ct.pinterest.com/v3/, the endpoint Pinterest's noscript tag and core.js use, with core.js's parameter names, the epik click id kept in _epik and event_id set for dedup with the Conversions API. |
A 403 means the connected Pinterest user lacks a Business Access role on the ad account (Admin, Analyst, Audience or Campaign).
Conversions API
Send server-side events to Pinterest with POST /v1/ads/conversions on your pinterestads account (Pinterest's POST /v5/ad_accounts/{id}/events). They deduplicate against the Pinterest tag by eventId, the same id the Shopify install sends.
| Field | |
|---|---|
destinationId | The numeric ad account id: Pinterest attributes to the ad account, not to a tag. GET /v1/accounts/{accountId}/conversion-destinations lists them. |
eventName | Standard names map to Pinterest's: Purchase = checkout, AddToCart, InitiateCheckout, AddPaymentInfo, Lead, CompleteRegistration = signup, Subscribe, ViewContent, Search, PageView = page_visit. Any other string is sent as is, so it matches a custom event of that name. |
user | Needs email, or ipAddress plus userAgent. Identifiers are hashed before they leave Zernio. |
user.clickIds.epik | The epik click id (the _epik cookie), sent as click_id. |
actionSource | web stays web; offline, crm, phone_call, system_generated and business_messaging become offline. App events need platformData.action_source app_android or app_ios. |
testCode | Any value sends the batch with test=true: Pinterest validates it without recording it. |
consent.adUserData: "DENIED" | Sets opt_out on every event. |
const { data } = await zernio.conversions.sendConversions({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
destinationId: '549755885175',
events: [{
eventName: 'Purchase',
eventTime: 1804150800,
eventId: 'order_1042',
value: 59.9,
currency: 'USD',
user: { email: 'ana@example.com', clickIds: { epik: 'dj0yJnU9...' } }
}]
}
});Response (200):
{
"platform": "pinterestads",
"eventsReceived": 1,
"eventsFailed": 0,
"failures": []
}Pinterest answers per event, so one bad event does not fail the batch: an event without an identity is listed in failures with code INVALID_EVENT before sending, and one Pinterest rejects with PINTEREST_EVENT_FAILED. The connected Pinterest user needs a Business Access role on the ad account (403 otherwise).
GET /v1/ads/conversions/quality?accountId=...&destinationId=<ad account id> (reference) returns Pinterest's Event Quality Score for Conversions API web events over the last 14 days: one row per event name, each identifier's coverage in matchKeys, and eventCoveragePercentage, the share of events whose event_id overlaps with the tag. Pinterest has no composite score, so compositeScore is absent.
What you cannot do
Pinterest Ads through Zernio does not support:
- Targeting or creative edits after creation.
PUT /v1/ads/{adId}takes status and budget;targetingorcreativereturns501with codeunsupported_platform_operation. - Campaign duplication.
POST /v1/ads/campaigns/{campaignId}/duplicatereturns501on Pinterest. - Ad-account scoping on the connect call, because Pinterest has no ads discovery.
- Income tier, city and radius targeting. Regions, metros and postal codes are supported.
- Catalog or shopping ads.
Common errors
| Error | Cause | Fix |
|---|---|---|
400 with code missing_required_field | One of headline, body, imageUrl or linkUrl is absent | Send all 4; Pinterest builds the Pin from them. |
400 with code invalid_field_value | A goal outside the 4 Pinterest values, or a headline over 100 or a body over 500 characters | Use a supported goal and trim the copy. |
403 with code ads_allowance_exceeded | The team has no payment method on file and has reached 500 live ads | Add a card to resume creating ads. |
501 with code unsupported_platform_operation | targeting or creative sent to PUT /v1/ads/{adId} | Change status or budget, and recreate the ad for anything else. |
502 with code platform_api_error | Pinterest rejected the request | Read platformError for Pinterest's own payload. |
A rejected create returns the flat envelope:
{
"error": "headline is required",
"type": "invalid_request_error",
"code": "missing_required_field",
"param": "headline"
}Error handling covers the envelope and the stable codes.
Related
- Pinterest: the posting account this ads account inherits its token from.
- Connecting accounts: the OAuth flow behind
authUrl. - Create standalone ad and Boost post: every field of both requests.
- Create ad audience and Add users to ad audience.
- Get ad analytics: metrics, date ranges and rollups.
- Pixels and tracking tags: the tag API across platforms.