PinterestAnswer

Pinterest API search: what v5 allows, and a keyword script

Pinterest's v5 API searches your own pins; its pin search by term is a beta. Our script saved 806 public pins for 'kitchen ideas' in 40 calls.

7 min read

Short answer

  • Pinterest's v5 API searches the boards and pins of the account that authorized your app. Its search by term, /v5/search/partner/pins, is a beta that is not open to all apps.
  • ToolzerHub's /v1/pinterest/search/pins searches public pins or videos by keyword, 18 to 25 pins a call, with image, outbound link, board, creation date and reactions, at 1 credit per call.
  • Our script saved 806 different pins for 'kitchen ideas' in 40 calls. 46% of the first 365 linked to a website, and 214 of 249 wedding-cake video pins linked to Instagram.
On this page

Pinterest's official API can search pins, but only those of the account that signed in to your app. Its search by keyword is a beta "not available to all apps". To search all of Pinterest by keyword, as you would on pinterest.com, use our Pinterest API; the script below saves the pins to CSV.

Pinterest's v5 API has three search endpoints. /v5/search/pins and /v5/search/boards search the pins and boards of the account behind the OAuth token. /v5/search/partner/pins takes a term and a country_code and returns up to 50 short pins a page, but it is still a beta. New apps start on Trial access, and wider use needs an upgrade request. If you publish pins, read your own analytics or run ads, use the official API: it is the only way to act on an account.

Search Pinterest with ToolzerHub

We make ToolzerHub. Our Pinterest API searches public pins without a Pinterest account or app review:

  • GET /v1/pinterest/search/pins?query=... returns one page of pins for a keyword. Add scope=videos for video pins only. Each response has a bookmark; send it back as bookmark=... for the next page.
  • GET /v1/pinterest/boards/pins?boardId=... lists the pins on a public board, and /v1/pinterest/profiles/boards lists a profile's boards.

Every call is a GET with the x-api-key header and costs 1 credit.

bash
export TOOLZERHUB_API_KEY=your_key
curl -G -H "x-api-key: $TOOLZERHUB_API_KEY" \
  --data-urlencode "query=kitchen ideas" \
  "https://api.toolzerhub.com/v1/pinterest/search/pins"

A response with 18 pins, trimmed to one, with the creator's name removed. The field names are Pinterest's own:

json
{
  "data": {
    "results": [
      {
        "id": "e1_AfqPShz3R431sNW1uDn-eJrr22p4ZLWBaUku450zFRi3SJcINS-rdw",
        "type": "pin",
        "title": "Cozy Modern Farmhouse Kitchen Ideas",
        "description": "Create a warm and inviting kitchen with elegant sage-green cabinets, ...",
        "link": null,
        "domain": "Uploaded by user",
        "created_at": "Thu, 01 Oct 2026 05:48:30 +0000",
        "reaction_counts": {"1": 13},
        "auto_alt_text": "a kitchen filled with lots of green cabinets and counter top space next to a window",
        "images": {
          "orig": {"width": 672, "height": 795, "url": "https://i.pinimg.com/originals/69/06/60/690660fb8a0f21dfdf4aaa54d3223e1e.jpg"}
        },
        "videos": null,
        "pinner": {"username": "…", "follower_count": 51},
        "board": {"id": "1109293020658371701", "name": "cozy kitchen home decor", "url": "/…/cozy-kitchen-home-decor/"}
      }
    ],
    "bookmark": "Y2JVSG81V2sxcmNHRlpWM1J5VFVad1ZsWlVWbFJXTVVZMldWVlZOV0pIU2ta..."
  }
}

link is the website a pin points to; null with "domain": "Uploaded by user" means the creator uploaded the image without a link. reaction_counts counts reactions, keyed by reaction type. A video pin has videos.video_list, where V_HLSV4 is an HLS playlist (.m3u8).

The id in search results is an opaque key starting with e1_, not the number in a pinterest.com/pin/ URL. It stays the same between calls, so it works for removing duplicates. The script's rows link to the image, the board and the outbound site.

Save Pinterest search results to CSV

The script follows the bookmark page by page, skips pins it has already seen and writes one row per pin. With --images it also downloads each pin's original image. Save it as pinterest_search.py and install requests (pip install requests).

python
import argparse
import csv
import os
import re
import sys
import time
from email.utils import parsedate_to_datetime
from pathlib import Path

import requests

URL = "https://api.toolzerhub.com/v1/pinterest/search/pins"
HEADERS = {"x-api-key": os.environ.get("TOOLZERHUB_API_KEY", "")}
FIELDS = ["pin_key", "title", "description", "image_url", "width", "height", "video_url",
          "link", "domain", "reactions", "created", "pinner", "board", "board_url", "alt_text"]


def search(query, scope, bookmark, tries=3):
    # 429, 5xx and dropped connections are retried; failed calls cost no credits.
    params = {"query": query, "scope": scope, **({"bookmark": bookmark} if bookmark else {})}
    for attempt in range(tries):
        last = attempt == tries - 1
        try:
            r = requests.get(URL, headers=HEADERS, params=params, timeout=120)
        except requests.RequestException:
            if last:
                raise
            time.sleep(10 * (attempt + 1))
            continue
        if r.status_code in (429, 500, 502, 503, 504) and not last:
            time.sleep(10 * (attempt + 1))
            continue
        if r.status_code == 401:
            sys.exit("the API key was rejected: check TOOLZERHUB_API_KEY")
        if r.status_code == 402:
            sys.exit("out of credits: the CSV keeps the pages written so far")
        r.raise_for_status()
        return r.json().get("data") or {}


def row(pin):
    image = (pin.get("images") or {}).get("orig") or {}
    video = ((pin.get("videos") or {}).get("video_list") or {}).get("V_HLSV4") or {}
    board = pin.get("board") or {}
    try:
        created = parsedate_to_datetime(pin["created_at"]).date().isoformat()
    except (KeyError, TypeError, ValueError):
        created = ""
    return {
        "pin_key": pin["id"],  # an opaque key, not the number in a /pin/ URL
        "title": (pin.get("title") or pin.get("grid_title") or "").strip(),
        "description": " ".join((pin.get("description") or "").split()),
        "image_url": image.get("url"), "width": image.get("width"), "height": image.get("height"),
        "video_url": video.get("url") or "",  # an HLS playlist (.m3u8)
        "link": pin.get("link") or "", "domain": pin.get("domain") or "",
        "reactions": sum((pin.get("reaction_counts") or {}).values()),
        "created": created,
        "pinner": (pin.get("pinner") or {}).get("username"), "board": board.get("name"),
        "board_url": "https://www.pinterest.com" + board["url"] if board.get("url") else "",
        "alt_text": pin.get("auto_alt_text") or "",
    }


def download(rows, folder):
    # Files are numbered in CSV row order.
    Path(folder).mkdir(parents=True, exist_ok=True)
    saved = failed = 0
    for i, r in enumerate(rows, 1):
        if not r["image_url"]:
            continue
        path = Path(folder) / f"{i:04d}{Path(r['image_url']).suffix or '.jpg'}"
        try:
            resp = requests.get(r["image_url"], timeout=60)
            resp.raise_for_status()
            path.write_bytes(resp.content)
            saved += 1
        except requests.RequestException:
            failed += 1
    return saved, failed


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("query")
    ap.add_argument("--pages", type=int, default=10)
    ap.add_argument("--scope", choices=["pins", "videos"], default="pins")
    ap.add_argument("--out")
    ap.add_argument("--images", help="folder to save the original images into")
    a = ap.parse_args()
    if not HEADERS["x-api-key"]:
        sys.exit("set TOOLZERHUB_API_KEY first")
    out = a.out or "pinterest_" + re.sub(r"\W+", "_", a.query).strip("_") + ".csv"
    seen, rows, repeats, pages, bookmark = set(), [], 0, 0, None
    with open(out, "w", newline="", encoding="utf-8-sig") as f:  # -sig: Excel reads UTF-8
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        for _ in range(a.pages):
            data = search(a.query, a.scope, bookmark)
            pages += 1
            for pin in data.get("results") or []:
                if pin.get("type") != "pin":  # skip any non-pin module in the results
                    continue
                if pin["id"] in seen:
                    repeats += 1
                    continue
                seen.add(pin["id"])
                rows.append(row(pin))
                writer.writerow(rows[-1])
            bookmark = data.get("bookmark")
            if not bookmark or bookmark == "-end-":
                break
    print(f"{len(rows)} pins from {pages} page(s) written to {out}, {repeats} repeats skipped")
    if a.images:
        saved, failed = download(rows, a.images)
        print(f"{saved} images saved to {a.images}/, {failed} failed")


if __name__ == "__main__":
    main()

Run it with a keyword. --pages caps the calls (default 10) and --scope videos keeps video pins:

bash
export TOOLZERHUB_API_KEY=your_key
python pinterest_search.py "kitchen ideas" --pages 40 --out deep_kitchen.csv
python pinterest_search.py "wedding cake" --scope videos
python pinterest_search.py "minimalist tattoo" --images tattoo_images
text
806 pins from 40 page(s) written to deep_kitchen.csv, 184 repeats skipped

The CSV columns are pin_key, title, description, image_url, width, height, video_url, link, domain, reactions, created, pinner, board, board_url and alt_text.

What the searches returned

We ran the script on four searches:

Search Calls Pins Linked out Made in 2026
kitchen ideas 15 365 167 (46%) 298 (82%)
kitchen ideas 40 806 458 (57%) 501 (62%)
wedding cake (videos) 10 249 230 (92%) 112 (45%)
minimalist tattoo 10 240 156 (65%) 127 (53%)
  • Pages hold 18 to 25 pins. The first 15 kitchen pages gave 365 pins with no repeats. The 40-page run returned all 365 again among its 806, plus 184 repeats the script skipped, so later pages add fewer new pins.
  • 198 of the first 365 kitchen pins were uploads with no outbound link. Of those that linked out, amazon.com was the most common site (23 pins), then homestratosphere.com (12).
  • Video search is mostly Instagram: all 249 wedding cake video pins had a video playlist, and 214 linked to Instagram.
  • 298 of the first 365 kitchen pins were created in 2026; the oldest was from 2018. The median pin had 4 reactions and the top one 4,619.
  • The tattoo run downloaded 240 original images, 64 MB in all, with a median width of 750 pixels.

What it costs

A call costs 1 credit and returns one page, so the 806 kitchen pins took 40 credits. Downloading the images costs no credits. You get 100 free credits a month; $19 buys 10,000 credits that never expire, and plans for higher volume cover more searches. Set your key and run the script on your first keyword.

For other visual search, see Xiaohongshu search in Python, and for keyword search on Threads, Threads API keyword search.

We are not affiliated with Pinterest.

Get this data from your own code

The Pinterest API returns it as JSON with one x-api-key header. 100 free credits monthly.