# Pinterest

Publish image and video pins to Pinterest boards, create boards, edit pin descriptions and read impressions, saves and clicks.

Pinterest works like a visual search engine. A pin keeps getting found for months, so the title, description, board and destination link matter more than the time you post. With Pozy you publish image and video pins with the same `POST /v1/posts` call you use everywhere else, using the platform id `pinterest`.

## What you can do

| Feature | Supported | Where |
| --- | --- | --- |
| Image pins | Yes, 1 image per pin | [Post types](#post-types) |
| Video pins | Yes, 1 video per pin, with a custom cover | [Post types](#post-types) |
| Choose or create a board | Yes | [Boards](#boards) |
| Scheduling | Yes | [Scheduling](https://pozy.ai/docs/publishing/scheduling) |
| Edit a published pin | Description only | [Edit a pin](#edit-a-published-pin) |
| Analytics | Impressions, saves, clicks | [Analytics](#analytics) |
| Inbox (DMs, comments) | No | Pinterest has no inbox API |
| Ads | Yes, separate connection | [Pinterest Ads](https://pozy.ai/docs/ads/pinterest-ads) |

There are no text-only pins, carousels or multi-image pins through the API. Every pin carries exactly one image or one video.

## Connect

Pinterest connects with OAuth. Request a connect URL for a profile and send the user to `authUrl`:

```bash cURL
curl "https://api.pozy.ai/v1/connect/pinterest?profileId=66a1f0c2a4b9d3e8f1a2b3c4&redirect_url=https://app.example.com/connected" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/connect/pinterest?profileId=66a1f0c2a4b9d3e8f1a2b3c4&redirect_url=https:%2F%2Fapp.example.com%2Fconnected", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
// Redirect the user's browser to data.authUrl
```
```python Python
import os, requests

resp = requests.get(
    "https://api.pozy.ai/v1/connect/pinterest",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={"profileId": "66a1f0c2a4b9d3e8f1a2b3c4", "redirect_url": "https://app.example.com/connected"},
)
print(resp.json()["authUrl"])
```

After the Pinterest consent screen the user picks a default board. Pozy hosts that screen for you. To build your own picker, pass `headless=true` and use [List Pinterest boards](https://pozy.ai/docs/api/listPinterestBoardsForSelection) and [Select Pinterest board](https://pozy.ai/docs/api/selectPinterestBoard). See [Hosted connect](https://pozy.ai/docs/connect/hosted-connect) and [Headless connect](https://pozy.ai/docs/connect/headless).

You can change the default board later with [Set default Pinterest board](https://pozy.ai/docs/api/updatePinterestBoards). No second OAuth is needed.

## Post types

`content` becomes the pin description. The board, title and destination link go in `platformSpecificData` on the Pinterest entry.

### Image pin

```bash cURL
curl -X POST https://api.pozy.ai/v1/posts \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Small kitchen layouts that still fit an island",
    "mediaItems": [{ "type": "image", "url": "https://pozy.ai/media/kitchen-2x3.jpg" }],
    "platforms": [{
      "platform": "pinterest",
      "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
      "platformSpecificData": {
        "boardId": "1022106146582011234",
        "title": "Small kitchen layouts with an island",
        "link": "https://blog.example.com/small-kitchens"
      }
    }],
    "publishNow": true
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    content: "Small kitchen layouts that still fit an island",
    mediaItems: [{ type: "image", url: "https://pozy.ai/media/kitchen-2x3.jpg" }],
    platforms: [{
      platform: "pinterest",
      accountId: "66b2e19d8c3f5a7e9d0b1c2d",
      platformSpecificData: {
        boardId: "1022106146582011234",
        title: "Small kitchen layouts with an island",
        link: "https://blog.example.com/small-kitchens",
      },
    }],
    publishNow: true,
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.post.platforms[0].platformPostUrl);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/posts",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "content": "Small kitchen layouts that still fit an island",
        "mediaItems": [{"type": "image", "url": "https://pozy.ai/media/kitchen-2x3.jpg"}],
        "platforms": [{
            "platform": "pinterest",
            "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
            "platformSpecificData": {
                "boardId": "1022106146582011234",
                "title": "Small kitchen layouts with an island",
                "link": "https://blog.example.com/small-kitchens",
            },
        }],
        "publishNow": True,
    },
)
print(resp.json()["post"]["platforms"][0]["platformPostUrl"])
```

> **Always send boardId:**
> 
> `boardId` is optional in the schema, but a pin without it lands on whichever board Pinterest lists first. If the account has no boards at all, the pin fails and asks for `platformSpecificData.boardId`.

### Video pin

Swap the media for one video. Set a cover with `coverImageUrl`, or let Pozy grab a frame at `coverImageKeyFrameTime` seconds:

```json
{
  "platform": "pinterest",
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "platformSpecificData": {
    "boardId": "1022106146582011234",
    "title": "Five-minute breakfast wrap",
    "link": "https://recipes.example.com/breakfast-wrap",
    "coverImageUrl": "https://pozy.ai/media/wrap-cover.jpg"
  }
}
```

### Platform fields

| Field | Type | Notes |
| --- | --- | --- |
| `boardId` | string | Board to pin to. Send it on every post. |
| `boardSectionId` | string | Section inside the board. Must belong to `boardId`, so send both. |
| `title` | string, up to 100 characters | Defaults to the first line of `content`. Searchable on Pinterest. |
| `link` | URL | Where a click on the pin goes. HTTPS, no URL shorteners. This is the field that drives traffic. |
| `coverImageUrl` | URL | Cover image for a video pin. |
| `coverImageKeyFrameTime` | integer (seconds) | Frame to use as the cover. Ignored when `coverImageUrl` is set. |
| `isAiGenerated` | boolean, default `false` | Adds Pinterest's "AI modified" disclosure. Pinterest may still label a pin from its own detection. |

The full list of shared post fields is on [Platform settings](https://pozy.ai/docs/publishing/platform-settings).

## Boards

List the boards on a connected account, or create one. A new board's `id` works as `boardId` straight away.

```bash cURL
# List boards
curl https://api.pozy.ai/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/pinterest-boards \
  -H "Authorization: Bearer $POZY_API_KEY"

# Create a board
curl -X POST https://api.pozy.ai/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/pinterest-boards \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Summer recipes", "description": "Quick dinners for hot days", "privacy": "PUBLIC" }'
```
```ts Node
const listRes = await fetch("https://api.pozy.ai/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/pinterest-boards", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const list = await listRes.json();
if (!listRes.ok) throw new Error(list.error);

const createdRes = await fetch("https://api.pozy.ai/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/pinterest-boards", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "Summer recipes", description: "Quick dinners for hot days", privacy: "PUBLIC" }),
});
const created = await createdRes.json();
if (!createdRes.ok) throw new Error(created.error);
console.log(created.board.id);
```
```python Python
base = "https://api.pozy.ai/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/pinterest-boards"
headers = {"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"}

boards = requests.get(base, headers=headers).json()["boards"]
created = requests.post(base, headers=headers, json={
    "name": "Summer recipes",
    "description": "Quick dinners for hot days",
    "privacy": "PUBLIC",
}).json()
print(created["board"]["id"])
```

`privacy` is `PUBLIC` (the default), `PROTECTED` or `SECRET`. Pinterest rejects a second board with the same name on one account.

## Edit a published pin

[Edit published post](https://pozy.ai/docs/api/editPost) replaces the description of a live pin, up to 800 characters. The title, media, link and board cannot change after publishing, and the pin keeps its id.

```bash
curl -X POST https://api.pozy.ai/v1/posts/65f1c0a9e2b5af0012ab34cd/edit \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "platform": "pinterest", "content": "Updated description with the new recipe notes" }'
```

> **Note:**
> 
> Pinterest still keeps its pin-update API behind an allowlist. Until Pinterest opens it, edit requests come back with Pinterest's "beta feature not yet enabled" error. Nothing on your side changes that. If the post went to several Pinterest accounts, pass `accountId` to choose which copy to edit.

## Media requirements

| Images | Requirement |
| --- | --- |
| Per pin | 1 |
| Formats | JPEG, PNG, WebP, GIF (animated GIFs count as images) |
| Max size | 32 MB (under 10 MB loads faster) |
| Minimum | 100 x 100 px |
| Best ratio | 2:3, for example 1000 x 1500 px. 1:1 and long pins up to 1:2.1 also work |

| Videos | Requirement |
| --- | --- |
| Per pin | 1 |
| Formats | MP4, MOV |
| Max size | 2 GB |
| Duration | 4 seconds to 15 minutes |
| Aspect ratio | 2:3, 1:1 or 9:16 |
| Recommended | 1080p, 25 fps or more, around 10 Mbps, AAC audio at 128 kbps |

Media URLs must be public, return the file bytes with the right `Content-Type`, and not redirect to a web page. Sharing links from Google Drive, Dropbox, OneDrive or iCloud return a page, not a file, and fail. Upload through [Media](https://pozy.ai/docs/publishing/media) to get a URL that always works.

## Limits

| Limit | Value |
| --- | --- |
| Title | 100 characters |
| Description | 800 characters, on create and on edit |
| Media | 1 image or 1 video per pin |
| Burst posting | Space pins out. Bursts of 10 or more at once trip Pinterest's rate limit |

Not available through Pinterest's API: Idea Pins, Rich Pins (they need meta tags on your site), website claiming, shopping catalogs, carousels and multi-image pins.

## Analytics

[Get post analytics](https://pozy.ai/docs/api/getAnalytics) with `platform=pinterest` returns, per pin:

| Metric | Field |
| --- | --- |
| Impressions | `impressions` |
| Saves | `saves` |
| Clicks | `clicks` |

```bash
curl "https://api.pozy.ai/v1/analytics?platform=pinterest&fromDate=2026-09-01&toDate=2026-09-30" \
  -H "Authorization: Bearer $POZY_API_KEY"
```

See [Post analytics](https://pozy.ai/docs/analytics/post-analytics) for paging and date ranges.

## Inbox

Pinterest has no DM, comment or review API, so Pinterest accounts do not appear in the [Inbox](https://pozy.ai/docs/inbox).

## Errors you may see

| Error | Cause | Fix |
| --- | --- | --- |
| "Pinterest requires a boardId" | No `boardId` and no board Pozy could fall back to | Send `platformSpecificData.boardId`. List boards first. |
| "Unable to reach the URL" | Pinterest could not download the media | Open the URL in a private window. It must load the raw file with no login or redirect. |
| "Invalid URL or request data" | Pinterest could not process the media or request | Check the URL is HTTPS and returns media bytes. |
| "Pinterest rate limit reached" | Too many pins in a short window | Spread pins out or use a [queue](https://pozy.ai/docs/publishing/queues). |

A post you publish right away can be accepted by Pozy and still fail on Pinterest. Check `post.status` and `platforms[].errorMessage` in the response, not only the HTTP status. [Errors](https://pozy.ai/docs/getting-started/errors) covers the error format.

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/accounts/{accountId}/pinterest-boards`](https://pozy.ai/docs/api/getPinterestBoards) | List Pinterest boards |
| [`POST /v1/accounts/{accountId}/pinterest-boards`](https://pozy.ai/docs/api/createPinterestBoard) | Create Pinterest board |
| [`PUT /v1/accounts/{accountId}/pinterest-boards`](https://pozy.ai/docs/api/updatePinterestBoards) | Set default Pinterest board |
| [`GET /v1/connect/pinterest/select-board`](https://pozy.ai/docs/api/listPinterestBoardsForSelection) | List Pinterest boards |
| [`POST /v1/connect/pinterest/select-board`](https://pozy.ai/docs/api/selectPinterestBoard) | Select Pinterest board |
| [`POST /v1/posts`](https://pozy.ai/docs/api/createPost) | Create post |
| [`POST /v1/posts/{postId}/edit`](https://pozy.ai/docs/api/editPost) | Edit published post |
| [`GET /v1/analytics`](https://pozy.ai/docs/api/getAnalytics) | Get post analytics |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`post.published`](https://pozy.ai/docs/webhooks/events#post-published) | Post published. |
| [`post.failed`](https://pozy.ai/docs/webhooks/events#post-failed) | Post failed. |
| [`post.platform.published`](https://pozy.ai/docs/webhooks/events#post-platform-published) | Post platform published. |
| [`post.platform.failed`](https://pozy.ai/docs/webhooks/events#post-platform-failed) | Post platform failed. |
| [`post.platform.deleted`](https://pozy.ai/docs/webhooks/events#post-platform-deleted) | Post platform deleted. |
