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:
titleon 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
urlto the outbound short link (etsy.me/…), the same string aslink. Keep the Pinterest permalink from the listing row (pinterest.com/pin/<id>/) as your join key. Do not assumedata.urlon details is always apinterest.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.