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

```bash cURL
curl "https://api.pozy.ai/v1/connect/pinterest/ads?profileId=66b2e19d8c3f5a7e9d0b1c2a" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
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);
```
```python Python
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())
```

```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 `authUrl` and `state`. Send the person to `authUrl`; the consent screen asks for `ads:read` and `ads:write` along 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 `400` asking 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.

```bash cURL
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
  }'
```
```ts Node
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);
```
```python Python
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"])
```

```json
{
  "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.

> **Pick the board:**
> 
> `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](https://pozy.ai/docs/platforms/pinterest)) and promote it with a boost.

### Budget and schedule

- `budgetAmount` is in whole units of the ad account currency, so `30` is $30.00 on a USD account. The minimum is 5.
- `budgetType` is `daily` or `lifetime`.
- `startDate` and `endDate` set the run window. Without them the Promoted Pin runs until you pause it.
- Some ad accounts require Campaign Budget Optimization. Those need a `lifetime` budget with an `endDate`, and a create without one returns `400` naming `endDate`.

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

```bash
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](https://pozy.ai/docs/ads/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](https://pozy.ai/docs/ads/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.

```bash
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 `pending` with 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.

```bash
curl "https://api.pozy.ai/v1/ads/66f0a1b2c3d4e5f6a7b8c9d0/analytics?fromDate=2027-01-04&toDate=2027-01-11" \
  -H "Authorization: Bearer $POZY_API_KEY"
```

- `reach` reads `0` on 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 `breakdowns` are not available for Pinterest ads.

[Reporting](https://pozy.ai/docs/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](https://pozy.ai/docs/ads/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](https://pozy.ai/docs/ads/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

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/connect/{platform}/ads`](https://pozy.ai/docs/api/connectAds) | Connect ads for a platform |
| [`GET /v1/ads/accounts`](https://pozy.ai/docs/api/listAdAccounts) | List ad accounts |
| [`POST /v1/ads/create`](https://pozy.ai/docs/api/createStandaloneAd) | Create standalone ad |
| [`POST /v1/ads/boost`](https://pozy.ai/docs/api/boostPost) | Boost post as ad |
| [`GET /v1/ads/tree`](https://pozy.ai/docs/api/getAdTree) | Get campaign tree |
| [`GET /v1/ads/{adId}`](https://pozy.ai/docs/api/getAd) | Get ad details |
| [`PUT /v1/ads/{adId}`](https://pozy.ai/docs/api/updateAd) | Update ad |
| [`PUT /v1/ads/{adId}/status`](https://pozy.ai/docs/api/updateAdStatus) | Pause or resume a single ad |
| [`PUT /v1/ads/ad-sets/{adSetId}`](https://pozy.ai/docs/api/updateAdSet) | Update an ad set |
| [`PUT /v1/ads/ad-sets/{adSetId}/status`](https://pozy.ai/docs/api/updateAdSetStatus) | Pause or resume a single ad set |
| [`PUT /v1/ads/campaigns/{campaignId}/status`](https://pozy.ai/docs/api/updateAdCampaignStatus) | Pause or resume a campaign |
| [`DELETE /v1/ads/{adId}`](https://pozy.ai/docs/api/deleteAd) | Cancel an ad |
| [`DELETE /v1/ads/ad-sets/{adSetId}`](https://pozy.ai/docs/api/deleteAdSet) | Delete an ad set |
| [`DELETE /v1/ads/campaigns/{campaignId}`](https://pozy.ai/docs/api/deleteAdCampaign) | Delete a campaign |
| [`GET /v1/ads/{adId}/analytics`](https://pozy.ai/docs/api/getAdAnalytics) | Get ad analytics |
| [`GET /v1/ads/interests`](https://pozy.ai/docs/api/searchAdInterests) | Search targeting interests |
| [`POST /v1/ads/audiences`](https://pozy.ai/docs/api/createAdAudience) | Create custom audience |
| [`POST /v1/ads/audiences/{audienceId}/users`](https://pozy.ai/docs/api/addUsersToAdAudience) | Add users to audience |
| [`GET /v1/accounts/{accountId}/tracking-tags`](https://pozy.ai/docs/api/listTrackingTags) | List tracking tags |
| [`POST /v1/accounts/{accountId}/tracking-tags`](https://pozy.ai/docs/api/createTrackingTag) | Create a tracking tag |
| [`POST /v1/ads/conversions`](https://pozy.ai/docs/api/sendConversions) | Send conversion events |
| [`GET /v1/ads/conversions/quality`](https://pozy.ai/docs/api/getConversionsQuality) | Get Event Match Quality |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`account.ads.initial_sync_completed`](https://pozy.ai/docs/webhooks/events#account-ads-initial-sync-completed) | Ads initial sync completed. |
| [`account.ads.sync_failed`](https://pozy.ai/docs/webhooks/events#account-ads-sync-failed) | Ads sync failed. |
| [`account.ads.sync_recovered`](https://pozy.ai/docs/webhooks/events#account-ads-sync-recovered) | Ads sync recovered. |
| [`ad.status_changed`](https://pozy.ai/docs/webhooks/events#ad-status-changed) | Ad status changed. |
| [`ad.review.approved`](https://pozy.ai/docs/webhooks/events#ad-review-approved) | Ad review approved. |
| [`ad.review.rejected`](https://pozy.ai/docs/webhooks/events#ad-review-rejected) | Ad review rejected. |
