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.
What Pinterest's v5 API can search
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. Addscope=videosfor video pins only. Each response has abookmark; send it back asbookmark=...for the next page.GET /v1/pinterest/boards/pins?boardId=...lists the pins on a public board, and/v1/pinterest/profiles/boardslists a profile's boards.
Every call is a GET with the x-api-key header and costs 1 credit.
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:
{
"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).
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:
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
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.