API GuidesAugust 23, 202611 min readingMarcel Czuryszkiewicz

Pinterest Pin API: Creating Image and Video Pins with v5

Most Pinterest guides stop at "find your board ID". This one starts there: resolving a board by name, the four-call video path with its S3 upload parameters, all six media source types from spec 5.23.0, and why a 201 Created can still leave your Pin visible to nobody but you.

The Pinterest Pin API creates a Pin with a single POST /v5/pins call, but that call never runs alone. Every Pin belongs to a board, and board_id sits in the request body, so publishing is always at least two operations: resolve the board, then create the Pin. Video takes four. This page covers both paths, all six media source types, and why a 201 Created does not mean anyone can see your Pin.

Verified 04.08.2026 against Pinterest's OpenAPI description 5.23.0, which Pinterest publishes as a versioned repository on GitHub, and against our own production integration.

Close-up of fine dry sand filling the whole frame, a single pale grain catching the light near the centre
Nothing gets pinned until there is something underneath to pin it to.

A Pin cannot exist without a board

board_id is a string matching ^\d+$. Spec 5.23.0 does not mark it required (PinCreate has no required array at all), but there is no route to a Pin without one. Treat it as mandatory.

Three situations follow. Only the first is what most guides describe.

You have the id. Send it.

You have a board name. GET /v5/boards first, match on name, read the id. Separate call, separate rate-limit bucket, and a paginated response: list boards returns a bookmark alongside the page, so an account with a few hundred boards needs a loop, not one request.

The board does not exist. Create it, then wait.

Our publish path runs all three in order: check the cached channel list, refresh it from Pinterest if the name is missing, and only then POST /v5/boards with privacy: PUBLIC. The full body that endpoint accepts is in Pinterest's create board reference. The refresh exists because the first version without it failed on accounts where the user had made the board minutes earlier. After creating one we sleep six seconds before re-reading the list: the id is in the create response immediately, but the list endpoint is not instantly consistent.

curl -s -X POST 'https://api.pinterest.com/v5/boards' \
  -H "Authorization: Bearer $PINTEREST_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name": "Autumn recipes", "privacy": "PUBLIC"}'

Every call here carries the same bearer token, and Pinterest's tokens expire. Our client assumes one hour when expires_in is missing, a guess worth removing; see OAuth token refresh for social APIs.

Creating an image Pin

With an id, the image path is one call. Every field below is from PinCreate 5.23.0, and the operation itself is Pinterest's create Pin reference.

FieldLimit / formatRequiredNote
board_idstring, ^\d+$Not per spec, yes in practicePinCreate has no required array
media_sourceone of six shapesNot per spec, yes in practiceDiscriminated on source_type
title≤ 100 chars, nullableNo
description≤ 800 chars, nullableNo
link≤ 2048 chars, nullableNoDestination URL, not the image
alt_text≤ 500 chars, nullableNo
dominant_colorhex, e.g. #6E7874, nullableNoPlaceholder while the image loads
board_section_id^\d+$, nullableNo
parent_pin_id^\d+$, nullableNoSet when saved from another Pin
curl -s -X POST 'https://api.pinterest.com/v5/pins' \
  -H "Authorization: Bearer $PINTEREST_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "board_id": "1234567890123456789",
    "title": "Roast pumpkin, four ways",
    "description": "The weeknight version, 40 minutes.",
    "link": "https://example.com/pumpkin",
    "alt_text": "Sliced roast pumpkin on a sheet pan",
    "dominant_color": "#6E7874",
    "media_source": {
      "source_type": "image_url",
      "url": "https://example.com/pumpkin.jpg"
    }
  }'

The response has no permalink field. Pin has seventeen properties and none links to the Pin; the string permalink appears nowhere in the 1.9 MB spec. Build it from the id: https://www.pinterest.com/pin/${pin.id}. pins/create also takes an optional ad_account_id query parameter, for Pinterest Business Access.

On maximum image size, nobody agrees. Our validator rejects images above 5 MB; one published guide states 20 MB; spec 5.23.0 gives no image size limit anywhere in PinCreate or the media source schemas. We have not established whether 5 MB is Pinterest's ceiling or ours, so do not hardcode a number from a blog post, including this one. That ambiguity is why we keep media requirements across platforms in one table.

A curled strip of 35mm colour negative film on a white surface, green-cast frames and AGFA edge markings in focus
Video takes the long route: register, upload, wait, then publish.

Video Pins: register, upload, poll, publish

Video is not a different parameter. It is a different API, on two hosts, with an asynchronous state in the middle.

1. Register the upload. POST /v5/media with {"media_type": "video"}, and video is the only value the enum accepts. The response carries everything for step two, and the field names are in Pinterest's register media upload reference:

{
  "media_id": "203014033110991560",
  "media_type": "video",
  "upload_url": "https://pinterest-media-upload.s3-accelerate.amazonaws.com/",
  "upload_parameters": {
    "key": "uploads/11/aa/22/3:video:203014033110991560:5212123920968240771",
    "policy": "eyJleHBpcmF0aW9uIjoiMj..==",
    "x-amz-algorithm": "AWS4-HMAC-SHA256",
    "x-amz-credential": "ASIA6QZJ64OPIKV7FRVX/20220127/us-east-1/s3/aws4_request",
    "x-amz-date": "20220127T185143Z",
    "x-amz-security-token": "IQoJb3JpZ2luX2VjEJr...==",
    "x-amz-signature": "fcd6309a...",
    "Content-Type": "multipart/form-data"
  }
}

2. POST the file to S3, with every one of those parameters. This request does not go to Pinterest and carries no bearer token. upload_parameters is a presigned POST policy: each key becomes a form field, and the file goes in a field literally named file. Drop one and S3 rejects the whole thing with an error that never mentions Pinterest.

curl -s -X POST "$UPLOAD_URL" \
  -F "key=$KEY" \
  -F "policy=$POLICY" \
  -F "x-amz-algorithm=AWS4-HMAC-SHA256" \
  -F "x-amz-credential=$CREDENTIAL" \
  -F "x-amz-date=$AMZ_DATE" \
  -F "x-amz-security-token=$SECURITY_TOKEN" \
  -F "x-amz-signature=$SIGNATURE" \
  -F "[email protected]"

3. Poll GET /v5/media/{media_id} until it settles. Status is one of registered, processing, succeeded, failed, and the media status reference is the only place Pinterest states what those values are. Our budget is eight checks, 20 seconds apart, roughly two and a half minutes. failed is terminal and stops the post; running out of checks is retryable, because "no answer yet" and "answered no" deserve different handling.

4. Create the Pin with video_id. media_id plus a cover: cover_image_url, base64 via cover_image_data, or a keyframe timestamp with cover_image_key_frame_time.

"media_source": {
  "source_type": "video_id",
  "media_id": "203014033110991560",
  "cover_image_url": "https://example.com/cover.jpg"
}

Video limits are where the documentation gets thin. The numbers in openapi.json (2 GB, 4 seconds to 15 minutes, .mp4/.mov/.m4v, 75 × 75 to 9450 × 9450 pixels) sit on the ad_video_*_link catalog attributes and in a catalog feed error example. Not on pins/create, not on any media schema. Our validator uses 2048 MB, 4 to 900 seconds, mp4 and quicktime, plus an aspect ratio band of 0.5 to 1.91 with no basis in the spec. Treat it as something to test against, not documented behaviour, and check whether the create Pin reference has grown a media schema since.

bundle.social

One request instead of four, across fifteen platforms.

Board resolution by name, the presigned S3 upload and the status poll, behind one call.

A felt-lined sorting tray on a wooden desk, each compartment holding a different embroidered patch design
One idea called media, sorted into six separate compartments.

Six media source types, not two

PinMediaSource is a oneOf with a discriminator on source_type, and it has six branches.

source_typeShapeWhen to useTrap
image_urlurlAnything hostedPinterest fetches it server-side; unreachable is a terminal failure, not a retry
image_base64content_type + dataImage not publicly reachablecontent_type takes only image/jpeg and image/png; data must match ^[a-zA-Z0-9+/=]+$, so strip the data:image/png;base64, prefix
video_idmedia_id + coverEvery video PinNeeds the register/upload/poll cycle first
multiple_image_urlsitems, 2–5 entriesCarouselminItems: 2, so a one-item array is a validation error
multiple_image_base64items, 2–5 entriesCarousel without hostingSame jpeg/png and base64 rules, per item
pin_urlsource_type onlyProduct PinsBeta, restricted to a list of accounts

The base64 prefix costs an afternoon. data:image/png;base64,iVBOR... fails the pattern on the first colon, and the error does not name the offending character.

One more gap: we send note, plus ai_disclosures for AI content. Neither appears anywhere in spec 5.23.0. Pinterest does not reject the unknown fields, but we have no evidence either is stored. The way to settle it is to read the Pin back through get Pin and see which properties survive.

Why your Pin is invisible: Trial vs Standard access

Nothing in the response looks wrong: 201 Created, a valid numeric id, a permalink you built from it, and no Pin on Pinterest.

Apps start on Trial access, and everything created there is a sandbox entity visible only to the account that made it. Not delayed, not under review. Invisible. Standard access makes created Pins real, and getting it means submitting the app for review.

Trial accessStandard access
Who sees created PinsOnly the creating accountEveryone
org_write budget300 calls per day, per app100 calls per minute, per user, per app
Overall request budget1000 per day100 per second
How you get itDefault on app creationSubmit the app for review

Those numbers come from Pinterest's access tier documentation, read in August 2026, not from openapi.json, which tags every operation with an x-ratelimit-category but never a number. Pinterest changes them without notice.

The tags still price a publish. POST /pins, POST /media and POST /boards are org_write; GET /boards and GET /media/{id} are org_read. One video Pin burns two org_write calls, three if the board must be created, plus up to nine org_read. At 300 a day, Trial is not many videos; see social media API rate limits.

Errors worth retrying and errors that are final

Pinterest returns {"code": <int>, "message": <string>} and publishes no dictionary of those codes. The spec's generic error example is {"code": 2, "message": "AdAccount not found."}: one example, for the whole numeric namespace. Everything below is our production classification, from observation, not a Pinterest table.

SignalWe treat it asBehaviour
Codes 8, 12, 30, 2787RetryableRe-queue with backoff
Codes 2, 58, 2786TerminalFail the post, no retry
Message already have a board with this nameTerminalBoard creation raced with itself
Message unable to reach the urlTerminalYour image_url is not fetchable from Pinterest
Message authentication failedTerminalToken problem, retrying wastes budget

Message matching sits alongside code matching because the same code turns up with different meanings. Splitting retryable from final is covered in social media API error handling.

What this looks like through bundle.social

Everything above is one platform. Our Pinterest API takes a boardName string, not a board_id, and the resolve-refresh-create sequence happens on our side, including the six-second wait after board creation. Video is a single request too: you hand over a file, and the registration, the presigned S3 POST, the status polling and the final POST /pins with video_id run as one durable job. Errors come back already classified retryable or terminal.

What it does not do: carousels are not exposed yet, and we cannot make your Trial-access app visible. That approval is between you and Pinterest, and no intermediary changes it. If you publish to Pinterest only, and you are comfortable owning a four-call video pipeline and a board cache, the direct API is a reasonable choice. The argument for an intermediary starts at the second platform, where the same video needs a different upload sequence for TikTok and a third for YouTube.

Frequently asked questions

Is board_id required to create a Pin?

In practice yes, though the spec does not say so. PinCreate in 5.23.0 has no required array, so nothing is formally mandatory, but there is no way to create a Pin outside a board. Resolve or create the board first, then send it as a string matching ^\d+$.

Why is my Pin not visible on Pinterest after a 201 response?

Almost always Trial access. Pins created by an app without Standard access are sandbox entities, visible only to the creating account. The API returns a real id and a real 201, which is why it reads as a bug rather than a permissions state.

How do I get the URL of a Pin I just created?

You build it. The Pin object in spec 5.23.0 has no permalink field, and the word appears nowhere in the spec. Concatenate the id yourself: https://www.pinterest.com/pin/{pin_id}. That is what we store as the permalink for every Pin we publish.

How long does Pinterest video processing take?

Pinterest publishes no figure; the media status endpoint is the only source of truth. Our budget is eight checks 20 seconds apart, about two and a half minutes, after which we treat it as retryable rather than failed. A failed status is terminal.

Can I create a carousel Pin through the Pinterest API?

Yes, with multiple_image_urls or multiple_image_base64, each taking an items array of 2 to 5 entries. Note the minimum: one item is a validation error, not a single-image Pin. bundle.social does not expose Pinterest carousels yet, so this is a direct-API call today.

Marcel Czuryszkiewicz
Written by

Marcel Czuryszkiewicz

Co-Founder

Marcel is one of the builders behind bundle.social, creating a social media API with a strong focus on developer experience, reliability, and real support. Before bundle.social, he worked at Samsung R&D, Reply AI, and Docplanner, building internal tools that helped companies grow. He brought that same practical, product-focused mindset to bundle.social, so you can trust that your project is in good hands.

Keep reading