API Guides

Pinterest API Guide 2026: Creating Pins with API v5, from Trial Access to Video and Carousel Pins

How to create pins with the Pinterest API v5 in 2026: Trial vs Standard access and what the upgrade review asks for, OAuth with 30-day tokens and continuous refresh, the scopes Create Pin needs, image, video, and carousel pins in Node and Python, rate limits with x-ratelimit-reset, and errors.
September 15, 2026 by Jesse Eisenbart14 min read
Oil painting of an alpine lake at midday with patches of red and white wildflowers scattered across a green hillside and snow peaks in the distance

TLDR: Pinterest API v5 lets you create image, carousel (2 to 5 images), and video pins with POST /v5/pins, and it is free. The catches are access and scopes. A new app gets Trial access, under which everything you create is visible only to you and limits are 1,000 requests a day; Standard access, which needs a video of your OAuth flow, makes pins public and raises writes to 100 a minute per user. Create Pin requires four scopes (boards:read, boards:write, pins:read, pins:write), not just pins:write. Tokens last 30 days with a 60-day continuous refresh token that rotates on each use. Descriptions allow 800 characters, titles 100, links 2,048. Video is a four-step upload through S3 with a mandatory cover image. Checked against the Pinterest OpenAPI spec (v5.28.0) and developer docs on September 15, 2026.

Does Pinterest have an API, and what can it do?

Pinterest API v5 is a REST API at api.pinterest.com/v5 covering user accounts, boards, pins, media uploads, analytics, ads, and catalogs. For a publishing integration the relevant surface is small:

Endpoint Purpose Scopes Rate category
GET /v5/user_account Who the token belongs to user_accounts:read org_read
GET /v5/boards, POST /v5/boards List and create boards boards:read, boards:write org_read, org_write
POST /v5/pins Create an image, carousel, or video pin boards:read, boards:write, pins:read, pins:write org_write
POST /v5/media, GET /v5/media/{id} Register and poll a video upload pins:read, pins:write org_write, org_read
GET /v5/pins/{id}/analytics Impressions, saves, clicks per pin pins:read org_analytics

What it does not do: create Idea Pins, publish GIF pins, edit a published pin’s media, or manage comments and messages. Every pin belongs to a board, so board selection is part of any integration, and Pinterest caps an account at 2,000 boards and 200,000 pins.

Pinterest is also unusual among the ten networks PostZen publishes to in that it charges nothing and reviews quickly, which makes it a good first platform to build. Our comparison of social media APIs puts it next to the other nine.

Pinterest API Trial access vs Standard access: what each tier allows

Every app starts in Trial access, and the rules of Trial are the thing most first-time integrators miss.

Trial access Standard access
Who sees what you create Only your own account; pins and boards are sandbox entities Everyone; content is public
Rate limits Per day, per app: 1,000 requests total, 300 writes Per minute, per user, per app: 100 writes, 1,000 reads; 100 requests a second overall
Review Each business day Rolling, no published turnaround
Requirements Business account, verified email, app details, privacy policy Trial approval, a working integration, a video of the OAuth flow

The practical consequence: you can build and test the whole integration on Trial, but no customer can use it, because pins they create through your app are invisible to their followers. Plan the Standard upgrade before you announce Pinterest support.

Pinterest’s docs list the common reasons for denial. Trial applications fail on an inaccessible or inaccurate privacy policy and vague app descriptions. Standard upgrades fail when the demo video does not show the OAuth authorization flow, when the app authenticates with anything other than OAuth (session cookies and stored credentials are called out), or when the recording shows wireframes rather than a live integration.

What the review asked us. PostZen’s app was upgraded to Standard access on July 27, 2026. The submission was a screen recording of the real product: a user clicking connect, Pinterest’s consent screen listing the scopes, the redirect back, board selection, and a pin created and appearing on Pinterest. Pinterest accepts terminal or Postman recordings if you are the only user of the app, but for a multi-user product the reviewers want to see the UI. Nothing about the review was slow; the mistake we made came after approval and is in the scopes section below.

A sandbox at api-sandbox.pinterest.com still exists for Trial apps, with the same endpoints, and Pinterest also issues product-limited tokens from the app dashboard that skip OAuth for 24 hours of testing. Once you move to production, point everything at api.pinterest.com and forget the sandbox exists; PostZen removed its sandbox override the day Standard access came through, because the sandbox board IDs do not exist in production.

How to set up Pinterest API access: app, OAuth, and tokens

  1. Convert or create a business account. Personal accounts cannot register apps.
  2. In the developer portal, accept the developer terms, register the app with a name, description, and privacy policy URL, and submit for Trial access. You can have up to five apps.
  3. Once approved, copy the app ID and secret and set a redirect URI.

The OAuth flow is the standard authorization code grant. Send the user to https://www.pinterest.com/oauth/ with client_id, redirect_uri, response_type=code, scope, and a state. Exchange the code at the token endpoint with HTTP Basic auth, where the username is the app ID and the password is the secret:

curl -X POST https://api.pinterest.com/v5/oauth/token \
  -u "$PINTEREST_APP_ID:$PINTEREST_APP_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=$CODE" \
  -d "redirect_uri=https://yourapp.com/oauth/pinterest" \
  -d "continuous_refresh=true"

The response has an access_token (prefixed pina), expires_in of 2,592,000 seconds (30 days), a refresh_token (prefixed pinr), and refresh_token_expires_in.

Token lifetimes changed in 2025 and the docs have not fully caught up. Pinterest used to issue a refresh token with a hard 365-day limit; that legacy token was deprecated in September 2025. Every app now gets a continuous refresh token: it expires 60 days after issue, and each refresh returns a new access token and a new refresh token with a fresh 60-day window, so a user who is refreshed at least every 60 days never has to log in again. Apps created before September 25, 2025 must send continuous_refresh=true to opt in; newer apps get it automatically. One of Pinterest’s own sample responses still shows a 365-day refresh_token_expires_in, which is the stale value. Store the new refresh token every time, since the old one is replaced.

Refreshing is the same endpoint with grant_type=refresh_token, and an expired or revoked token returns HTTP 401 with error code 2.

Which scopes does the Pinterest Create Pin API need?

The full v5 scope list is ads:read, ads:write, billing:read, billing:write, biz_access:read, biz_access:write, boards:read, boards:write, boards:read_secret, boards:write_secret, catalogs:read, catalogs:write, pins:read, pins:write, pins:read_secret, pins:write_secret, user_accounts:read, and user_accounts:write.

For publishing you need five, and four of them are required by the Create Pin endpoint itself:

Scope Why
user_accounts:read Identify the connected account
boards:read List boards; required by Create Pin
boards:write Create boards; also required by Create Pin
pins:read Required by Create Pin and by media polling
pins:write Create pins and register media

That boards:write requirement is the one that bit us. The day PostZen went to Standard access we trimmed the scope list to what looked like the minimum for publishing and dropped boards:write, assuming it only mattered for a test helper that created boards. Live publishing broke immediately with a 403 naming the missing scope, and we reverted the same day. The Create Pin reference lists all four scopes, and it means them. If you want to publish to secret boards, add the four _secret scopes as well; PostZen requests them as optional so a user who declines them stays connected for public boards.

Scopes are granted per token. If you add a scope later, existing users must reconnect, and PostZen’s publish preflight checks all four Create Pin scopes before attempting a pin so a partial grant fails with “reconnect your Pinterest account” instead of a raw 403.

How to create a pin with the Pinterest API

POST /v5/pins takes a JSON body. The fields and their limits from the OpenAPI spec:

Field Limit Notes
board_id Numeric string Practically required; the pin lives here
board_section_id Numeric string Optional section within the board
title 100 characters Shown and searchable
description 800 characters Not 500; 500 is Pinterest’s recommendation for display
link 2,048 characters Destination URL; no domain claim needed for a plain link
alt_text 500 characters Accessibility text
dominant_color Hex string Optional
media_source Object image_url, image_base64, multiple_image_urls, multiple_image_base64, or video_id

The note field that older tutorials mention was removed; Pinterest deprecated pin notes in September 2025.

An image pin from a URL in Node, using the built-in fetch:

const PINTEREST = 'https://api.pinterest.com/v5';

async function pinterest(path, body, token) {
  const res = await fetch(`${PINTEREST}${path}`, {
    method: body ? 'POST' : 'GET',
    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json();
  if (!res.ok) {
    const err = new Error(`Pinterest ${res.status} code ${json.code}: ${json.message}`);
    err.status = res.status;
    err.resetSeconds = Number(res.headers.get('x-ratelimit-reset') ?? 0);
    throw err;
  }
  return json;
}

const pin = await pinterest('/pins', {
  board_id: process.env.BOARD_ID,
  title: 'A calm home office',
  description: 'Three changes that made a small desk feel bigger. Full walkthrough on the blog.',
  link: 'https://example.com/small-desk',
  alt_text: 'A bright desk beside a window with a chair and a small plant.',
  media_source: { source_type: 'image_url', url: 'https://cdn.example.com/desk.jpg' },
}, process.env.PINTEREST_TOKEN);

console.log(pin.id, pin.board_id, pin.created_at);

The same in Python with requests. Pinterest’s only official SDK is Python, and it covers ad campaign management rather than pins and boards, so plain HTTP is the normal route:

import os
import requests

PINTEREST = "https://api.pinterest.com/v5"
HEADERS = {"Authorization": f"Bearer {os.environ['PINTEREST_TOKEN']}"}


def create_pin(board_id: str, image_url: str, **fields):
    body = {
        "board_id": board_id,
        "media_source": {"source_type": "image_url", "url": image_url},
        **fields,
    }
    r = requests.post(f"{PINTEREST}/pins", json=body, headers=HEADERS, timeout=30)
    if r.status_code == 429:
        raise RuntimeError(f"rate limited; retry in {r.headers.get('x-ratelimit-reset')}s")
    r.raise_for_status()
    return r.json()


pin = create_pin(
    os.environ["BOARD_ID"],
    "https://cdn.example.com/desk.jpg",
    title="A calm home office",
    description="Three changes that made a small desk feel bigger.",
    link="https://example.com/small-desk",
    alt_text="A bright desk beside a window with a chair and a small plant.",
)
print(pin["id"])

Image rules, from Pinterest’s product specs: PNG or JPEG (everything is converted to JPEG on upload), 20 MB per file uploaded from the web and 32 MB from the apps, and a recommended 2:3 portrait ratio at 1000×1500 pixels. Images taller than 2:3 get cropped in the feed. The image_base64 source accepts only image/jpeg and image/png content types.

The response is the full pin object: id, board_id, created_at, media with the processed image URLs, and the fields you sent. There is no post URL field; the pin’s page is https://www.pinterest.com/pin/{id}/.

A carousel is the same endpoint with multiple_image_urls and 2 to 5 items:

{
  "board_id": "1234567890",
  "title": "Five desk setups under $200",
  "description": "Swipe through all five. Links to each item on the blog.",
  "link": "https://example.com/desk-setups",
  "media_source": {
    "source_type": "multiple_image_urls",
    "items": [
      { "url": "https://cdn.example.com/desk-1.jpg", "title": "Setup one", "description": "Walnut and white" },
      { "url": "https://cdn.example.com/desk-2.jpg" },
      { "url": "https://cdn.example.com/desk-3.jpg" }
    ],
    "index": 0
  }
}

Each item can carry its own optional title, description, and link, and index selects which image is the cover. The carousel source is fully present in the OpenAPI spec but has been trimmed from Pinterest’s narrative docs, which is why several schedulers do not offer it. PostZen publishes a Pinterest target with two to five images as a carousel automatically.

How to create a video pin with the Pinterest API

Video is a four-step flow, because the file goes to S3 rather than to Pinterest directly.

// 1. Register the upload
const media = await pinterest('/media', { media_type: 'video' }, token);
// media.media_id, media.upload_url, media.upload_parameters

// 2. POST the file to S3 as multipart/form-data: every upload_parameter first, the file last, no Authorization header
const form = new FormData();
for (const [key, value] of Object.entries(media.upload_parameters)) form.append(key, value);
form.append('file', new Blob([videoBytes], { type: 'video/mp4' }), 'reel.mp4');
const upload = await fetch(media.upload_url, { method: 'POST', body: form });
if (upload.status !== 204) throw new Error(`S3 upload failed: ${upload.status}`);

// 3. Poll until processed
let status;
do {
  await new Promise((r) => setTimeout(r, 5000));
  status = (await pinterest(`/media/${media.media_id}`, null, token)).status;
} while (status === 'registered' || status === 'processing');
if (status !== 'succeeded') throw new Error('Pinterest could not process the video');

// 4. Create the pin
const pin = await pinterest('/pins', {
  board_id: process.env.BOARD_ID,
  title: 'Desk tour in 20 seconds',
  media_source: {
    source_type: 'video_id',
    media_id: media.media_id,
    cover_image_url: 'https://cdn.example.com/desk-cover.jpg',
  },
}, token);

Two details cost people time. The S3 step returns 204 No Content on success and a 4xx with an XML body on failure, and the upload_parameters must all be included, in a form, before the file field. And a cover is mandatory: POST /v5/pins returns 400 for a video_id source with no cover_image_url and no cover_image_key_frame_time. The key frame option takes a whole-second offset into the video and is the easy default.

Video specs: MP4, MOV, or M4V with H.264 or H.265, up to 2 GB and 15 minutes, at least 4 seconds, and an aspect ratio between 1:2 and 1.91:1 with square or vertical recommended. Status values are registered, processing, succeeded, and failed. PostZen caps Pinterest videos at 150 MB for now; that is our upload path’s buffer limit, not Pinterest’s, and the Pinterest platform docs state it.

Pinterest boards: creating, listing, and choosing one

Every pin needs a board, so a publishing integration has to list boards and let the user pick, or create one. GET /v5/boards paginates with bookmark and page_size (default 25, maximum 250) and returns each board’s id, name, privacy, and pin count. POST /v5/boards takes name, an optional description, and privacy of PUBLIC, PROTECTED, or SECRET. Board section names are capped at 180 characters; the spec sets no explicit length on the board name itself.

The design decision is where the board choice lives. Passing board_id on every request is simplest for a script. For a product with many users, storing a default board per connected account and letting a request override it avoids asking for a board every time, and it is what PostZen does: the connect flow ends with board selection, the default is saved on the account, and a request can pass settings.boardId to override. Secret boards need the _secret scopes and are the most common reason a board a user can see in Pinterest does not appear in the API’s list.

Pinterest API rate limits and how to handle 429s

Pinterest publishes its limits by category, and the two that matter for publishing are writes and reads:

Category Covers Trial (per day, per app) Standard (per minute, per user, per app)
org_write Create, edit, delete pins and boards; register media 300 100
org_read User account, boards, pins, media status 1,000 1,000
org_analytics Pin and account analytics 1,000 60
Universal Everything 1,000 per day 100 per second

Because Standard limits are per user, connecting more accounts does not eat into the ones you already have; a scheduler publishing for a thousand users has a thousand separate 100-a-minute budgets. Every response carries three headers:

x-ratelimit-limit: 100, 100;w=1, 1000;w=60
x-ratelimit-remaining: 99
x-ratelimit-reset: 1

The limit header lists windows (w=1 for the per-second ceiling, w=60 for the per-minute category), and x-ratelimit-reset is seconds until the window clears. When you get a 429, sleep for Retry-After if present, otherwise for x-ratelimit-reset, then retry. PostZen’s Pinterest publisher does exactly that inside its retry queue, so a scheduled pin that hits the limit is deferred rather than failed. Pinterest’s docs also say you can file a ticket to request a higher limit.

Pinterest API errors and what they mean

Errors are a JSON object with an integer code and a message, and the HTTP status carries most of the meaning:

Status Typical cause What to do
400 Missing cover on a video pin, bad board_id, oversize text, unsupported media Fix the request; the message names the field
401 with code 2 Expired or revoked token Refresh, or reconnect if the refresh token is gone
403 Token lacks a required scope, or the board is not yours Reconnect with the full scope set; the message says “sufficient permissions”
404 Board or pin ID does not exist for this user Re-list boards; sandbox IDs never exist in production
429 Rate limit Wait for x-ratelimit-reset and retry

Pinterest does not publish a table of numeric codes beyond the examples in its spec, so key handling off the HTTP status. All API partners are also subject to Pinterest’s spam and abuse monitoring; a burst of identical pins to many boards is the pattern most likely to trip it.

Reading pin analytics from the Pinterest API

Once pins are live, GET /v5/pins/{pin_id}/analytics returns daily and lifetime metrics including IMPRESSION, SAVE, PIN_CLICK, OUTBOUND_CLICK, and for video VIDEO_START, VIDEO_MRC_VIEW, and VIDEO_AVG_WATCH_TIME. Account-level totals and top pins come from GET /v5/user_account/analytics and /user_account/analytics/top_pins. These sit in the org_analytics category at 60 requests a minute on Standard access, which is tight for a dashboard that polls every pin; batch with the multi-pin analytics endpoint or poll on a schedule. PostZen’s Pinterest analytics collect impressions, clicks, saves, comments, and reactions per pin and feed the best-time-to-post model, as described in our social media analytics overview.

Creating pins through PostZen instead

Everything above, plus token refresh every 60 days, the S3 form upload, the four-scope preflight, board defaults, and the 429 backoff, is what PostZen’s Pinterest integration does behind one request. The user connects through a hosted OAuth link that requests the nine scopes and ends with board selection; you send:

curl -X POST https://api.postzen.dev/v1/posts \
  -H "Authorization: Bearer $POSTZEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Three changes that made a small desk feel bigger. Full walkthrough on the blog.",
    "publishNow": true,
    "mediaItems": [{ "url": "https://cdn.example.com/desk.jpg" }],
    "platforms": [
      {
        "platform": "pinterest",
        "accountId": "jh72r5nqk9wx3v8m1t4cz6bs0fy5dg3e",
        "settings": {
          "boardId": "1234567890",
          "title": "A calm home office",
          "link": "https://example.com/small-desk",
          "altText": "A bright desk beside a window with a chair and a small plant."
        }
      }
    ]
  }'

One image makes an image pin, two to five make a carousel, one video makes a video pin with coverImageUrl or coverImageKeyFrameTime in settings. Swap publishNow for scheduledFor and the pin waits in PostZen’s scheduler until its time. The same request can carry Instagram, TikTok, and the other platforms alongside, as the post to all social media at once guide shows, and the Pinterest integration page lists what the target supports. Pinterest’s own docs are at the developer portal, and the OpenAPI spec on GitHub is the more reliable reference when the two disagree.

Frequently asked questions

Does Pinterest have an API for creating pins?

Yes. Pinterest API v5 has a POST /v5/pins endpoint that creates image, carousel, and video pins on a board. You need a business account, an app approved for at least Trial access, and an OAuth token with the boards:read, boards:write, pins:read, and pins:write scopes.

What is the difference between Pinterest Trial access and Standard access?

Trial access exposes the full API, but every pin and board you create is visible only to your own account and limits are counted per day per app, with 300 writes and 1,000 requests a day. Standard access makes content public and moves limits to per minute per user, with 100 writes a minute. Upgrading requires a video of your OAuth flow and a working integration.

Is the Pinterest API free?

Yes. There is no fee for Pinterest API v5. Access is gated by app review: Trial access is reviewed each business day, and the upgrade to Standard access is reviewed on a rolling basis with no published turnaround.

How long do Pinterest API tokens last?

Access tokens last 30 days. Since September 2025 Pinterest issues only continuous refresh tokens, which last 60 days and are replaced on every refresh, so an integration that refreshes at least every 60 days never needs the user to log in again. Apps created before September 25, 2025 must pass continuous_refresh=true to opt in.

What are the Pinterest API rate limits?

On Standard access, pin and board writes are limited to 100 requests per minute per user per app, reads to 1,000 per minute, and everything to 100 requests per second. Trial access allows 1,000 requests a day in total and 300 writes. Responses carry x-ratelimit-limit, x-ratelimit-remaining, and x-ratelimit-reset headers, and exceeding a limit returns HTTP 429.

How do you post a video pin with the Pinterest API?

Register the upload with POST /v5/media, send the file as a multipart form to the returned S3 upload URL with the supplied parameters, poll GET /v5/media/{id} until status is succeeded, then call POST /v5/pins with media_source.source_type set to video_id and a cover_image_url or key frame time. Video must be MP4, MOV, or M4V, up to 2 GB and 15 minutes.