Flaghand

Guides · Updated

How to create a Pin with the Pinterest API

To post a Pin, send POST https://api.pinterest.com/v5/pins with a board_id and a media_source (an image URL, base64 image data, 2 to 5 images, or an uploaded video), plus an optional title (100 characters), description (800), link (2,048) and alt_text (500). Authenticate with an OAuth user access token that has the boards:read, boards:write, pins:read and pins:write scopes. A new app gets Trial access: 300 write requests a day for the whole app, Pin creates included, and every Pin it creates is visible only to the person who made it. Public Pins need Standard access, which Pinterest grants after reviewing a video of your app.

Checked on November 3, 2026 against Pinterest's developer docs: Create Pin, creating boards and Pins, authentication and scopes, access tiers, rate limits and Sandbox, Pinterest's OpenAPI description (v5.28.0) and its Pin specs.

What you need first

  • A Pinterest business account to own the app.
  • An app with Trial access. In My apps, accept the developer terms, choose Connect app, fill in the form and submit it. Pinterest reviews each request and emails you the answer. Once approved, My apps shows the app ID and app secret.
  • A redirect URI added on the app's Configure tab. It has to match the one you send exactly.
  • An access token for the account that posts. To try the API on your own account, Pinterest lets a Trial app generate a test token without OAuth; it lasts 24 hours. For anyone else, use the OAuth flow below.

Get a token with OAuth

Send the person to Pinterest's consent page:

https://www.pinterest.com/oauth/?client_id=YOUR_APP_ID&redirect_uri=https%3A%2F%2Fyour.app%2Fcallback&response_type=code&scope=boards:read,boards:write,pins:read,pins:write&state=RANDOM_STATE

Pinterest sends them back to your redirect URI with a code and your state, which you check. Exchange the code for a token, signing in with your app ID and secret as HTTP Basic credentials:

curl -X POST https://api.pinterest.com/v5/oauth/token \
  -u "$PINTEREST_APP_ID:$PINTEREST_APP_SECRET" \
  --data-urlencode grant_type=authorization_code \
  --data-urlencode code="$CODE" \
  --data-urlencode redirect_uri=https://your.app/callback

The answer holds an access_token (it starts with pina and lasts 30 days, expires_in 2,592,000) and a refresh_token (it starts with pinr). Refresh before the 30 days are up with grant_type=refresh_token. The refresh token lasts 60 days and renews each time you use it. A token can stop working early, for example when the person changes their password, and Pinterest doesn't always tell you; a 401 means start the flow again.

Post it

Every Pin goes on a board, and the API never picks one for you. List the account's boards with GET /v5/boards and use the board's id.

curl

# 1. List the boards. Each item has an id and a name.
curl "https://api.pinterest.com/v5/boards?page_size=250" \
  -H "Authorization: Bearer $PINTEREST_TOKEN"

# 2. Create the Pin on one of them.
curl -X POST https://api.pinterest.com/v5/pins \
  -H "Authorization: Bearer $PINTEREST_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"board_id\": \"$BOARD_ID\",
       \"title\": \"Weeknight tomato soup\",
       \"description\": \"Ready in 25 minutes, with pantry staples.\",
       \"link\": \"https://example.com/recipes/tomato-soup\",
       \"alt_text\": \"A bowl of tomato soup with basil on a wooden table\",
       \"media_source\": {\"source_type\": \"image_url\", \"url\": \"https://example.com/images/tomato-soup.jpg\"}}"

Pinterest answers 201 Created with the new Pin. Its id is the Pin's ID:

{
  "id": "654321654321654321",
  "board_id": "123456123456123456",
  "title": "Weeknight tomato soup",
  "link": "https://example.com/recipes/tomato-soup",
  "created_at": "2026-11-03T15:04:05",
  "media": { "media_type": "image" }
}

Pinterest downloads the image from url, so it has to be reachable from the internet. To send the file itself, use {"source_type": "image_base64", "content_type": "image/jpeg", "data": "<base64>"}; content_type is image/jpeg or image/png.

Python

Pinterest's own Python SDK covers ads and campaigns. For Pins, requests is all you need:

import os

import requests

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

# Find the board by name. Boards come in pages; follow the bookmark to the end.
boards, params = [], {"page_size": 250}
while True:
    page = requests.get(f"{API}/boards", headers=headers, params=params, timeout=30)
    page.raise_for_status()
    page = page.json()
    boards += page["items"]
    if not page.get("bookmark"):
        break
    params["bookmark"] = page["bookmark"]
board = next(b for b in boards if b["name"] == "Recipes")

resp = requests.post(
    f"{API}/pins",
    headers=headers,
    json={
        "board_id": board["id"],
        "title": "Weeknight tomato soup",
        "description": "Ready in 25 minutes, with pantry staples.",
        "link": "https://example.com/recipes/tomato-soup",
        "alt_text": "A bowl of tomato soup with basil on a wooden table",
        "media_source": {"source_type": "image_url", "url": "https://example.com/images/tomato-soup.jpg"},
    },
    timeout=30,
)
resp.raise_for_status()
print(resp.json()["id"])

Node.js (18 or later)

const API = 'https://api.pinterest.com/v5';
const auth = { Authorization: `Bearer ${process.env.PINTEREST_TOKEN}` };

async function pinterest(path, init = {}) {
  const r = await fetch(`${API}${path}`, { ...init, headers: { ...auth, ...init.headers } });
  if (!r.ok) throw new Error(`Pinterest answered ${r.status}: ${await r.text()}`);
  return r.json();
}

// Find the board by name, following the bookmark through every page.
const boards = [];
let bookmark = null;
do {
  const page = await pinterest(`/boards?page_size=250${bookmark ? `&bookmark=${encodeURIComponent(bookmark)}` : ''}`);
  boards.push(...page.items);
  bookmark = page.bookmark;
} while (bookmark);
const board = boards.find((b) => b.name === 'Recipes');

const pin = await pinterest('/pins', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    board_id: board.id,
    title: 'Weeknight tomato soup',
    description: 'Ready in 25 minutes, with pantry staples.',
    link: 'https://example.com/recipes/tomato-soup',
    alt_text: 'A bowl of tomato soup with basil on a wooden table',
    media_source: { source_type: 'image_url', url: 'https://example.com/images/tomato-soup.jpg' },
  }),
});
console.log(pin.id);

To put the Pin in a board section, add board_section_id. To post 2 to 5 images as one Pin, use {"source_type": "multiple_image_urls", "items": [{"url": "…"}, {"url": "…"}]}, or multiple_image_base64 with content_type and data for each item.

Trial access: your Pins are visible only to you

This is the surprise most first Pins hit. The call succeeds, you get a Pin ID, and nobody else can see the Pin. Pinterest's access tiers page says all Pins and boards created with Trial access are visible only to their creator. You see them on your own profile; your followers don't.

To make Pins public, upgrade the app to Standard access: choose Upgrade on the app's card in My apps, confirm your use case and privacy policy, and upload a video of your app sending a user through the OAuth flow and doing something with the API. Pinterest asks for the video even if you are the app's only user, and accepts a screen recording of calls in a terminal or Postman. Standard also raises the rate limits.

Two more things stop first Pins:

  • Business accounts. If your app was registered on or after September 14, 2026, Pinterest may answer 403 with PINNER_DATA_ACCESS_DENIED when you read a board or Pin of a personal account. Connect business accounts.
  • Sandbox can't test video. Pinterest's Sandbox (https://api-sandbox.pinterest.com/v5, with a Sandbox token from My apps) takes the same calls without touching production, but can't create video Pins. Don't create a Sandbox Pin on a group board: the board's owner may get a notification.

Video Pins: register, upload, wait, create

A video takes four calls. Register the upload with POST /v5/media, upload the file to the upload_url Pinterest gives you, wait until GET /v5/media/{media_id} says succeeded, then create the Pin with source_type video_id and a cover:

import os
import time

import requests

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

# 1. Register the upload.
reg = requests.post(f"{API}/media", headers=headers, json={"media_type": "video"}, timeout=30)
reg.raise_for_status()
reg = reg.json()

# 2. Upload the file with every upload parameter. No Pinterest token here; a success is 204.
with open("soup.mp4", "rb") as f:
    up = requests.post(reg["upload_url"], data=reg["upload_parameters"], files={"file": f}, timeout=600)
up.raise_for_status()

# 3. Wait for Pinterest to process it.
while True:
    status = requests.get(f"{API}/media/{reg['media_id']}", headers=headers, timeout=30)
    status.raise_for_status()
    status = status.json()["status"]
    if status == "succeeded":
        break
    if status == "failed":
        raise RuntimeError("Pinterest couldn't process the video")
    time.sleep(5)

# 4. Create the Pin.
resp = requests.post(
    f"{API}/pins",
    headers=headers,
    json={
        "board_id": os.environ["BOARD_ID"],
        "title": "Weeknight tomato soup",
        "media_source": {
            "source_type": "video_id",
            "media_id": reg["media_id"],
            "cover_image_url": "https://example.com/images/tomato-soup-cover.jpg",
        },
    },
    timeout=30,
)
resp.raise_for_status()
print(resp.json()["id"])

Pinterest's guide says a valid cover image URL avoids a 400. Instead of a URL, you can send cover_image_data with cover_image_content_type, or pick a frame with cover_image_key_frame_time in whole seconds; past the video's end, Pinterest uses the last frame. A new video Pin can show creative_type REGULAR for a few seconds before it turns VIDEO.

Limits

Limit
Title 100 characters
Description 800 characters
Alt text 500 characters
Link 2,048 characters
Images in one Pin 2 to 5, with multiple_image_urls or multiple_image_base64
Image files BMP, JPEG, PNG, TIFF or WebP, up to 20 MB on the web. Base64 uploads take JPEG or PNG
Video files MP4, MOV or M4V, H.264 or H.265, 4 seconds to 5 minutes
All requests, Trial 1,000 a day
All requests, Standard 100 a second, per user, per app
Creating Pins and media uploads (org_write), Trial 300 a day, per app
Creating Pins and media uploads (org_write), Standard 100 a minute, per user, per app
Reading boards, Pins and media (org_read), Trial 1,000 a day, per app
Reading boards, Pins and media (org_read), Standard 1,000 a minute, per user, per app
Access token 30 days. The refresh token lasts 60 days and renews when used

Past a limit, Pinterest answers 429. The x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset headers show where you are. Pinterest says its rate limits can change without notice; to ask for more, open an API Technical Support request in its Help Center.

Do it yourself, or let an agent draft it through Flaghand

Call the API yourself for a script that pins to your own business account, or for an app you'll take through Standard review. It's a few calls, and nothing waits for a person.

If an AI agent writes the Pins, a token with pins:write lets it post anything, to any board, at once. Flaghand is a publishing desk for that case. You connect a Pinterest business account with Pinterest's own sign-in. Your agent connects to Flaghand's MCP server (https://app.flaghand.com/mcp) or its API, lists your boards live and drafts an image Pin, a 2-to-5-image Pin or a video Pin with a cover, with its title, description, link, alt text, board and section. Flaghand never picks a board for it. Before you approve, Flaghand checks the board still exists and measures the Pin against these limits: 100-character title, 800-character description, 500 characters of alt text, a 2,048-character link, JPEG, PNG or WebP images, and MP4, M4V or MOV video of 4 seconds to 5 minutes, read from the file itself. You approve the exact Pin with your passkey; Flaghand posts it at its time and reads it back to check its title, description, link, board, section, alt text and media type. No token Flaghand issues can approve, schedule or post. Flaghand's Pinterest access is in Trial today, so Pinterest shows the Pins it posts only to you for now, and the approval sheet says so. Board lists are read live and never kept. Sign-up takes no card (current terms). Get started with Flaghand.

More on keeping an agent's posts in your hands: let an AI agent post safely and a social media approval workflow.

Questions

How do I get Pinterest API access? Sign in to a Pinterest business account, accept the developer terms in My apps, choose Connect app and submit the form. Pinterest reviews the request and emails you. Approval gives Trial access; public Pins and higher limits need Standard access, which takes a video of your app's OAuth flow.

Why can't anyone else see the Pin I created with the API? Your app has Trial access. Pinterest makes every Pin and board a Trial app creates visible only to its creator. Upgrade the app to Standard access to post public Pins.

Which scopes does creating a Pin need? Pinterest's docs list boards:read, boards:write, pins:read and pins:write for Create Pin. Listing boards needs boards:read. Secret boards and Pins need boards:read_secret, boards:write_secret, pins:read_secret or pins:write_secret.

How many Pins can I create through the Pinterest API? With Trial access, up to 300 a day for the whole app: Pin creates share that limit with every other write, inside 1,000 requests a day overall. With Standard access, 100 a minute for each user of your app.

Why does the Pinterest API answer PINNER_DATA_ACCESS_DENIED? Apps registered on or after September 14, 2026 may get this 403 when they read a board or Pin of a personal account. Connect a business account instead.

Can an AI agent post to Pinterest for me? With your token it can post anything, immediately. Through Flaghand it drafts and asks for approval, and only your passkey approval of the exact Pin makes it go out.

Sources

Checked November 3, 2026: