Engineering

Pinterest API: what it takes to get a pin posted

Pinterest has a real, free, public REST API. The parts that cost developers a week are the access tier that makes your pins invisible, the board_id every pin requires, and a refresh token rule that changed. Here is the whole path to a live pin.

Pinterest is the rare platform where the API is not the hard part. It is a normal REST API, v5, free to use, OAuth 2.0, JSON in and JSON out. You can read the whole surface in an afternoon.

What actually costs people a week is everything around the HTTP call: an access tier that lets your pins succeed and stay invisible, a board_id that is mandatory on every single pin and has no default, a video path that is a four-step async job, and a refresh-token rule that changed in late 2025 so half the tutorials online are now wrong.

This is the path from zero to a pin that a stranger can see, with the failure modes in the order you will actually hit them.

What the Pinterest API can and cannot do for posting

Start here, because two of these rows are the reason people give up on Pinterest halfway through a sprint.

Capability

Supported

What it costs you

Image pin

Yes

One call. Pinterest fetches the image from a public URL.

Carousel pin (multiple images)

Yes

One call, an array of image URLs.

Video pin

Yes

Four steps: register, upload, poll, create. Cover image required.

List and create boards

Yes

Paginated with a bookmark cursor.

Pin analytics

Yes

Impressions, saves, pin clicks, outbound clicks, by date.

Account analytics

Yes

Same metric names, account level, date range.

Delete a pin

Yes

Straightforward DELETE.

Comments and replies

No. There are no organic comments endpoints in the v5 reference.

You cannot read or write pin comments through the API at all.

Repin / repost

No. Not exposed.

If your product promises reshares, Pinterest is not in scope.

Text-only posts

No. Every pin needs media.

An image or a video is mandatory, not optional.

The comments gap is worth naming loudly. If you are building a social inbox, an engagement tool, or an agent that replies to people, Pinterest simply cannot be part of it. That is a platform decision, not an integration you have not found yet.

Registering the app, and the tier that decides whether anyone sees your pins

You need a Pinterest business account and an app registered in the developer portal. That part is quick. The part to slow down for is the access tier.

Newly approved apps get trial access. Pinterest's own wording is the clearest statement of the problem: all pins and boards created with trial access are only visible to their creator as sandbox entities. Your POST /v5/pins returns a 201 with a real pin ID. Nothing errors. Nothing warns you. The pin is just not there for anyone else.

If you are testing your integration by asking a colleague to open the pin URL and they say it 404s, your code is fine. Your access tier is not.

To get out of trial and publish pins the public can see, you apply for standard access. Pinterest requires that your app is already approved for trial, that it complies with the Developer Guidelines, and that you submit a video recording of your app completing an action using the Pinterest API. There is no way around the recording: Pinterest states that even if you are the only intended user of the API, a video of the OAuth flow is still required. You submit it from My apps with the Upgrade button on the app card, and requests are reviewed in batches rather than instantly.

Budget calendar time for this, not engineering time. We keep a per-platform breakdown of these gates in social media API approval by platform — Pinterest belongs to the group whose APIs return success while the content stays invisible, alongside TikTok and YouTube. It is the most expensive category of surprise there is, because your tests pass.

Scopes: ask for five, not two

Pinterest scopes are comma-separated in the authorize URL. The set you need to create a pin end to end is larger than it first looks.

Scope

Why you need it

pins:write

Create the pin. The obvious one.

pins:read

Read the pin back, and read pin analytics.

boards:read

Resolve board_id values. You cannot post without this.

boards:write

Create a board on the user's behalf, e.g. a first-run setup flow.

user_accounts:read

Profile info, follower and pin counts, account analytics.

The one people forget is boards:read. Posting requires a board ID, board IDs come from the boards endpoint, and the boards endpoint needs that scope. An integration that requested only pins:write can authenticate perfectly and still be unable to post anything, because it has no legal value to put in board_id.

Adding a scope later means sending every connected user back through the OAuth screen. Ask for the full set on day one.

Tokens: 30 days, and the refresh rule changed

Token exchange is HTTP Basic auth — base64(client_id:client_secret) in the Authorization header — with a form-encoded body, not JSON. That trips people who arrive from Meta's APIs. The numbers below come from Pinterest's authentication guide.

  • Access tokens last 30 days (expires_in: 2592000).
  • Refresh tokens are now continuous refresh tokens: 60-day expiry, refreshable indefinitely.
  • The legacy 365-day refresh token is no longer supported. If you are reading a guide that says Pinterest refresh tokens last a year, it predates the change and will cost you a reconnect.
  • continuous_refresh=true only applies to apps created before 25 September 2025. Apps created on or after that date get continuous refresh automatically and should ignore the parameter.

The operational consequence: you must refresh each user's token at least once every 60 days, forever, or they silently drop off and have to reconnect. For a product with real users this is a scheduled job, not a try/catch around a 401. Budget for the job before you launch, because the failure lands 60 days after launch — precisely when nobody is watching the logs any more.

Board IDs: the mandatory field with no default

Every pin belongs to a board. There is no profile feed to post to, no default board, no implicit destination. board_id is required on POST /v5/pins and a request without it is rejected.

So any Pinterest posting flow has a discovery step in front of it. Boards come back from GET /v5/boards, paginated with a bookmark cursor and up to 100 per page. You have to loop until the bookmark stops coming back:

bash
1# List boards, one page at a time, until the bookmark runs out.
2curl -s "https://api.pinterest.com/v5/boards?page_size=100" \
3  -H "Authorization: Bearer $PINTEREST_ACCESS_TOKEN"
4
5# Then, while response.bookmark is present:
6curl -s "https://api.pinterest.com/v5/boards?page_size=100&bookmark=$BOOKMARK" \
7  -H "Authorization: Bearer $PINTEREST_ACCESS_TOKEN" 

Two things to handle while you are in there:

  • Board privacy is PUBLIC, PROTECTED or SECRET. Pinning to a secret board is the second way to publish a pin that nobody can see, and unlike the trial-access trap it will still be true after you reach standard access. If your UI shows users a board picker, show the privacy value next to the name.
  • Board lists are not stable. Users rename, merge and delete boards. Cache the ID, not the name, and re-resolve when a pin fails with a board error.

If you would rather not write the pagination loop, our Pinterest API exposes it as one call — GET /v1/pinterest/accounts/{id}/boards aggregates every page and returns the full list with a count. The same thing is available to an LLM as the list_pinterest_boards tool on our Pinterest MCP server, which is the piece that makes "post this to my Recipes board" work without the agent having to know what a bookmark cursor is.

Media: two completely different paths

Images and videos do not share a code path. Get this wrong and you will write an uploader you never needed.

Images: no upload step at all

You hand Pinterest a publicly reachable HTTPS URL and Pinterest fetches it itself. There is no multipart upload, no media ID, no polling.

  • source_type: "image_url" with a single url for one image.
  • source_type: "multiple_image_urls" with an items array for a carousel.

The failure mode is entirely about reachability. Pinterest's fetcher is not your browser and not your test suite. A URL behind auth, behind a signed link that expires in 60 seconds, behind a redirect chain, or on a host that rate-limits unknown user agents will produce a 400 telling you a valid image URL must be provided. Serve media from something boringly public with a long TTL.

Videos: register, upload, poll, then create

  1. POST /v5/media with {"media_type": "video"} returns a media_id, an upload_url and a set of upload_parameters.
  2. POST the file to that URL as multipart/form-data, with the upload parameters as fields first and the file part last.
  3. Poll GET /v5/media/{media_id} until status is succeeded. It moves through registered and processing, and can land on failed.
  4. Create the pin with source_type: "video_id", the media_id, and a cover image.

Three things bite here, in our experience running this path:

  • The upload is an S3 browser POST and needs an exact Content-Length. Chunked transfer encoding gets you a 411, not a helpful error.
  • A video pin needs a cover image. You can supply one explicitly, or let Pinterest generate one from a keyframe timestamp — but a video pin with no cover at all is not a thing.
  • A failed media status is where the real reason lives. The documented status object is thin, and Pinterest often returns extra fields alongside it that explain the failure. Log the entire payload on failed, not just the status string, or you will be debugging blind.

The specs, from Pinterest's own pin specs page

Pinterest publishes these under Review Pin specs. They apply to pins however they are created, API included.


Images

Video

Formats

BMP, JPEG, PNG, TIFF, WEBP

MP4, M4V on web; MOV also accepted on mobile

Max file size

20 MB on web

See Pinterest's specs; encode H.264 or H.265

Length

n/a

Minimum 4 seconds, maximum 5 minutes

Title

100 characters

100 characters

Description

800 characters

800 characters

Those two character limits are the ones that will truncate your copy silently if you pipe content in from another network. A LinkedIn post body is very often longer than 800 characters; a pin description is not allowed to be.

Rate limits: the numbers, and the one that hurts

Pinterest meters by endpoint category, and the tier changes the unit as well as the number. This is the part of the Pinterest API that most affects architecture, so here are the categories that matter for posting:

Category

What it covers

Trial access

Standard access

Overall

All API requests

1,000 per day

100 per second per user per app

org_write

Creating, editing or deleting boards, board sections or pins

300 per day per app

100 per minute per user per app

org_read

Fetching user accounts, boards, board sections or pins

1,000 per day per app

1,000 per minute per user per app

org_analytics

User-related analytics, account info, top pins

1,000 per day per app

60 per minute per user per app

Read the units, not just the numbers. On trial access the budget is per app, not per user. A multi-tenant product on trial access shares one 300-writes-per-day bucket across every customer it has. On standard access the same category becomes 100 per minute per user per app, which is a different universe: sustained, that is 144,000 writes a day for a single user, against 300 a day for your entire application. It also stops your customers from starving each other.

That is the real reason standard access matters. The sandbox problem is what you notice first; the per-app metering is what would have broken you at scale anyway.

Every response carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset. Read them rather than counting requests yourself — Pinterest states plainly that all rate limits are subject to change without notice, and a hardcoded limiter is a limiter that is wrong eventually. Pinterest's rate limit reference is the source for the table above.

A working create-pin request

With standard access, a valid board ID and a publicly reachable image, the whole thing is one call to POST /v5/pins:

bash
1curl -X POST https://api.pinterest.com/v5/pins \
2  -H "Authorization: Bearer $PINTEREST_ACCESS_TOKEN" \
3  -H "Content-Type: application/json" \
4  -d '{
5    "board_id": "987654321098765432",
6    "media_source": {
7      "source_type": "image_url",
8      "url": "https://cdn.example.com/lookbook/summer-dress.jpg"
9    },
10    "title": "Summer collection 2026",
11    "description": "Linen dresses, made to be worn twice a week.",
12    "link": "https://example.com/collections/summer",
13    "alt_text": "A model wearing a red linen summer dress"
14  }'

The response is the full pin object. The field you need to keep is id — it is the handle for analytics and deletion later, and Pinterest will not give it to you again:

json
1{
2  "id": "813744226420795884",
3  "board_id": "987654321098765432",
4  "link": "https://example.com/collections/summer",
5  "title": "Summer collection 2026",
6  "alt_text": "A model wearing a red linen summer dress"
7}

(Trimmed — the live response carries more fields, including media and creation timestamp. IDs are illustrative.)

For a video pin the same call takes {"source_type": "video_id", "media_id": "...", "cover_image_url": "..."} instead, once the media has finished processing.

Failure modes, in the order you will meet them

Symptom

Cause

201 created, nobody else can see the pin

Trial access. Pins are sandbox entities visible only to their creator.

201 created, still invisible on standard access

The board is PROTECTED or SECRET.

400, "a valid image url must be provided"

Pinterest could not fetch your image: auth, expiry, redirect or a blocked fetcher.

400 on every create

Missing board_id, or no media at all. Text-only pins do not exist.

401 after roughly a month

Access token hit its 30-day expiry and nothing refreshed it.

Reconnect prompts after two months

The 60-day continuous refresh token lapsed. Refresh on a schedule, not on error.

411 on the video upload

Chunked transfer encoding. The S3 upload needs an exact Content-Length.

Media stuck, then failed

Codec, duration or size. The reason is in the full status payload, not the status field.

429

Category limit. Check the three x-ratelimit headers before retrying.

Doing it through Outstand

Everything above is what the direct integration costs. If you would rather not own it, Outstand is a social media api that puts Pinterest behind the same endpoint as every other network we support. A pin is a post with a pinterest config block:

bash
1curl -X POST https://api.outstand.so/v1/posts \
2  -H "Authorization: Bearer $OUTSTAND_API_KEY" \
3  -H "Content-Type: application/json" \
4  -d '{
5    "content": "Linen dresses, made to be worn twice a week.",
6    "accounts": ["pinterest"],
7    "containers": [
8      {
9        "content": "Linen dresses, made to be worn twice a week.",
10        "media": [{ "url": "https://cdn.example.com/summer-dress.jpg", "filename": "summer-dress.jpg" }]
11      }
12    ],
13    "pinterest": {
14      "board_id": "987654321098765432",
15      "title": "Summer collection 2026",
16      "link": "https://example.com/collections/summer",
17      "alt_text": "A model wearing a red linen summer dress"
18    }
19  }'

The video path, the media polling, the cover-image rule, the bookmark pagination and the 60-day refresh schedule are on our side of that line. When a refresh does fail for good — the user revoked access, or the account changed — we emit an account.token_expired webhook, so you hear about a dead connection from your own system rather than from the customer. The full field reference is in the Pinterest configuration docs.

Two things we will not pretend away, because they are Pinterest's rules and not ours:

  • The platform limits are still the platform's. No amount of abstraction creates comments, repins or text-only pins that Pinterest does not offer, and no proxy makes a rate-limit category larger than Pinterest sets it.
  • Managed credentials mean a shared app identity. Outstand ships a managed Pinterest app so you can skip the developer portal and the upgrade video, and for most teams that is the whole point. What you inherit with it is that app's access tier and standing with Pinterest, not your own — and the OAuth screen carries our app, not your brand. If you need your own app identity, your own review relationship, or a rate-limit budget that answers only to you, bring your own keys. Both modes are supported, and the honest answer depends on which of those you care about.

Either way, the order of operations does not change: register the app, request all five scopes, get through the upgrade review with your recording, resolve a board ID, then post. The last step is the easy one.