Back to blog
Pinterest APIPinterest boardssocial media API

How to get Pinterest boards and pins as JSON

CaptapiOctober 10, 20264 min read
TL;DR
Pinterest's official API is for your own boards. One GET returns a public board's pins — and why a list card's null title is not the pin.
How to get Pinterest boards and pins as JSON

Pinterest's official API is an OAuth product for your own boards. To read a public board, a profile's pins, or a single pin as JSON, the working call is GET /v1/pinterest/board (about 0.5 credits per pin, minimum 2) plus GET /v1/pinterest/pin-details at 1 credit when a list card is missing a title.

Every field name and value below comes from production responses captured on October 10, 2026 against Etsy's public profile. Nothing is paraphrased from docs.

A public board, as JSON

Etsy's "Artwork for Your Home" board (pinCount 471) returned a 5-pin page. The first row:

{
  "id": "155303888286105350",
  "url": "https://www.pinterest.com/pin/155303888286105350/",
  "title": null,
  "description": "Wildly playful illustrations give new life to vintage dishes…",
  "link": "https://etsy.me/4hqceQB",
  "saves": 1,
  "board": {
    "name": "Artwork for Your Home",
    "url": "https://www.pinterest.com/etsy/artwork-for-your-home/",
    "pinCount": 471
  },
    "originAuthor": { "id": "155304024558534660" }
}

The envelope on that call was requested: 5, listingHits: 50, totalReturned: 5, hasMore: true, nextCursor: "5", truncatedReason: "pidgets-window". Native pidgets dumps a window of about 25–100 pins — that is the ceiling, not the board's pinCount. A null nextCursor means the window ended, not that the board is empty.

The call

JavaScript:

const res = await fetch(
  "https://api.captapi.com/v1/pinterest/board?" +
    new URLSearchParams({
      url: "https://www.pinterest.com/etsy/artwork-for-your-home/",
      limit: "20",
    }),
  { headers: { Authorization: `Bearer ${process.env.CAPTAPI_KEY}` } },
);
const { data } = await res.json();
for (const pin of data.pins) {
  console.log(pin.id, pin.title, pin.link, pin.saves);
}
if (data.hasMore) console.log("next", data.nextCursor);

Python:

import httpx, os

r = httpx.get(
    "https://api.captapi.com/v1/pinterest/board",
    params={"url": "https://www.pinterest.com/etsy/artwork-for-your-home/", "limit": 20},
    headers={"Authorization": f"Bearer {os.environ['CAPTAPI_KEY']}"},
    timeout=60,
)
data = r.json()["data"]
for pin in data["pins"]:
    print(pin["id"], pin.get("title"), pin.get("link"), pin.get("saves"))
print("hasMore", data["hasMore"], "cursor", data.get("nextCursor"))

A profile's boards are the same shape on /v1/pinterest/user-boards. Etsy returned 5 boards on a limit=5 page; the first was that artwork board, followers: null. Board followers is omitted-as-null until a board-scoped count exists — Pinterest's logged-out follower figure is account-scale and is never copied onto every board.

The trap: a list card is not a pin page

On the board listing, that vintage-plates pin had title: null. The same pin through /v1/pinterest/pin-details filled the page headline:

{
  "id": "155303888286105350",
  "title": "One-of-a-kind Painted Vintage Plates",
  "description": "Wildly playful illustrations give new life to vintage dishes…",
  "link": "https://etsy.me/4hqceQB",
  "createdAt": "2026-10-07T01:03:13Z",
  "author": { "username": "etsy", "displayName": "Etsy" },
  "imageOriginal": "https://i.pinimg.com/originals/d7/1b/97/d71b97311d7adfc4c8eaae488474f262.png"
}

Two things to treat as pin-level, not call-level:

  • title on a list row is whatever the widget card shipped. Null means the card was blank, not that the pin has no title. Hydrate the ones you will show.
  • On this capture, pin-details set url to the outbound short link (etsy.me/…), the same string as link. Keep the Pinterest permalink from the listing row (pinterest.com/pin/<id>/) as your join key. Do not assume data.url on details is always a pinterest.com/pin/ URL.

Keyword search is a different surface. /v1/pinterest/search hydrates Google/DDG site:pinterest.com/pin hits — logged-out Pinterest search HTML is empty. A q=scandinavian kitchen call on the same day returned totalReturned: 0 and truncatedReason: "serp-window". That is the SERP window, not a 502. Empty search is 0 credits.

Profile pins vs a named board

/v1/pinterest/user-pins on etsy is the profile dump, not one board. Same envelope (listingHits: 50, nextCursor: "5"). The first pin sat on a board named Products whose pinCount was 1040533498 — a catalog figure, not a pageable archive. saves on that row was null. Board-scoped saves showed up on the artwork board (saves: 1). Treat null saves as unknown, not zero.

Every pin row that has an image also ships image, imageOriginal, and images{236x,564x,originals,…} when pidgets exposes the size map. Pin-details on the vintage plates used the originals URL on both image and imageOriginal.

What not to page forever

Etsy's _products board reported pinCount: 1040533498 on a user-pins card. That is Pinterest's catalog number, not a pageable archive. The pidgets window still caps around 100. If you need one pin's title, outbound URL, and createdAt, call pin-details. If you need a board's current public window, call board and stop when hasMore is false.

Pin images live on i.pinimg.com. Signed social CDNs expire — how to read the expiry before a stored URL 404s.

The board endpoint page has the full pin row and the pidgets-window envelope. A free key is 100 credits — enough to pull a board window and hydrate the titles that came back null.