Pinterest Visual Research API Tutorial | SandBase
Build a Pinterest visual-research workflow: list a brand's boards, read a board's pins, and widen with search — one SandBase key, no Pinterest login.

If you research visual trends on Pinterest — how a brand curates, what a board collects, what’s surfacing for a theme — you want a repeatable loop: list a handle’s boards, open the ones that matter, and widen the net with keyword search. This Pinterest Visual Research API tutorial wires that loop with three SandBase endpoints so an agent can run it end to end. It builds on the Pinterest public data API hub; read that first for the big picture.
Everything here is public, read-only data. There is no Pinterest login and no SDK — a SandBase API key is still required to authenticate. The endpoint API reference is the source of truth for parameters and the response envelope; the payload field names below come from calls I ran (tested on 2026-09-27, UTC) and are observed-only — not documented guarantees — so confirm them against a live response, since payloads change over time.
Key takeaway
- Three endpoints form the loop:
user-boards→board→search.- Each call is
POST /v1/api/pinterest/<path>with only that endpoint’s params, oneSANDBASE_API_KEY.- The envelope can vary: the reference documents
outputs[0].data, but in my calls the payload often arrived under a top-leveloutputobject with asuccessflag, a result list, and acursor— read either shape.- Chain by natural inputs: a
handlelists boards, each board’surlopens its pins, and aquerywidens the set.
The workflow at a glance
- List boards for a handle with
pinterest/user-boards. - Open a board with
pinterest/boardusing a boardurlfrom step 1. - Widen with search using
pinterest/searchon aqueryto catch pins beyond that brand.
Each endpoint returns the same envelope — an id, a status, and the model. The reference documents the payload under outputs[0].data; in my calls it often arrived under a top-level output object carrying success, a result list, and a cursor. A failed or timeout run carries error. Read either shape (prefer output, fall back to outputs[0].data) and write the status check once.
This tutorial uses the endpoint-specific Model API references and their POST https://api.sandbase.ai/v1/api/pinterest/... paths. The Pinterest catalog cards expose a separate GET /apis/v1/pinterest/... surface — do not substitute the catalog path. Each step below links its exact reference.
SandBase vs. Pinterest’s official platform
| Your need | Use |
|---|---|
| Public, read-only visual research | SandBase Pinterest public-data API |
| Post, save pins, or act as a member | Pinterest’s official platform |
| Private or account-only data | Neither public workflow |
The Pinterest endpoints on SandBase — user-boards, board, and search drive this loop.
Step 0: one helper for every call
import os
import requests
API = "https://api.sandbase.ai/v1/api"
HEADERS = {
"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
"Content-Type": "application/json",
}
def call(path: str, payload: dict) -> dict:
resp = requests.post(f"{API}/{path}", headers=HEADERS, json=payload, timeout=70)
resp.raise_for_status()
body = resp.json()
if body.get("status") != "completed":
error = body.get("error", {})
raise RuntimeError(error.get("message", f"{path} did not complete"))
# Envelope can vary: prefer a top-level `output`, else `outputs[0].data`.
output = body.get("output")
if output is None and body.get("outputs"):
output = body["outputs"][0].get("data", {})
return output or {}
Every step reads named fields defensively with .get().
Step 1: list a handle’s boards
out = call("pinterest/user-boards", {"handle": "nike"})
boards = out.get("boards", []) if out.get("success") else []
print(len(boards), "boards")
In the response I captured (user-boards run id 962cce4c-cfc8-4fb1-8ed4-943109e4dd97, tested on 2026-09-27, UTC), the output carried a success flag, a boards list, and a cursor. Each board carried a name, a url, a pin_count, and a description. Field names move over time — treat these as observed and confirm against a live response.
first = next((b for b in boards if isinstance(b, dict) and b.get("url")), None)
board_url = first.get("url") if first else None
Step 2: open a board’s pins
out = call("pinterest/board", {"url": board_url})
pins = out.get("pins", []) if out.get("success") else []
cursor = out.get("cursor")
print(len(pins), "pins, more:", bool(cursor))
In my run (board run id 74fa4c3d-6e7d-4c0b-9d7a-61787d564a65), the payload carried a success flag, a pins list, and a cursor. To page, resend board with the returned cursor — confirm the exact parameter name against the board API reference.
The board reference — the source of truth for the url parameter and the response path.
Step 3: widen with search
out = call("pinterest/search", {"query": "minimalist kitchen"})
pins = out.get("pins", []) if out.get("success") else []
print(len(pins), "search pins")
The search endpoint takes a query and returns pins with a cursor (search run id 7298c08a-09be-471b-978a-b155a323d9e3 in my run). This widens your set beyond one brand’s boards to whatever is surfacing for a theme. See the search API reference for the exact parameters.
The search reference — pass a query; page with the returned cursor.
Putting it together
def research(handle: str, query: str):
boards_out = call("pinterest/user-boards", {"handle": handle})
boards = boards_out.get("boards", []) if boards_out.get("success") else []
collected = []
for b in boards:
if not isinstance(b, dict) or not b.get("url"):
continue
board_out = call("pinterest/board", {"url": b["url"]})
collected.append({
"board": b.get("name"),
"pins": board_out.get("pins", []) if board_out.get("success") else [],
})
search_out = call("pinterest/search", {"query": query})
return {
"boards": collected,
"search": search_out.get("pins", []) if search_out.get("success") else [],
}
Because all three endpoints share the same envelope, the loop stays flat: one status check in call(...), one output.success guard, and one .get() pattern everywhere. Identifiers flow from step to step — a handle yields boards, each board’s url yields pins.
Practical notes
- Envelope can vary. The reference documents
outputs[0].data; in my calls the payload often arrived under a top-leveloutputobject with asuccessflag. Read either shape (preferoutput, fall back tooutputs[0].data) and branch onstatus. - Board needs a valid
url. Take it from auser-boardsresult rather than constructing it. - Page with the
cursor. Bothboardandsearchreturn acursor; resend it for the next page. Confirm the parameter name against the reference. - Business fields are observed-only. The reference guarantees the envelope; treat
boards,pins,pin_count, and the like as observed and confirm live. - Public, read-only data only. No posting, saving, or private/account-only data. Authenticate with a SandBase API key.
- Be a good client. Retry with backoff on transient errors such as HTTP 429; page with the cursor rather than hammering.
FAQ
Do I need a Pinterest login or SDK?
No. You authenticate to SandBase with your SANDBASE_API_KEY. These read endpoints need no Pinterest account or OAuth on your side.
Why does this surface return output instead of outputs?
The reference documents the payload under outputs[0].data; in my calls it often arrived under a top-level output object with a success flag, a result list, and a cursor. Read either shape (prefer output, fall back to outputs[0].data), then check success and read the list defensively.
How do I get a board url?
From user-boards. Each board in output.boards carries a url you pass straight to pinterest/board.
How do I page through more pins?
Both board and search return a cursor in output. Resend the same call with the cursor to fetch the next page. Confirm the exact parameter name against the reference.
Wrap up
Three endpoints, one output envelope, identifiers flowing between steps — that is the whole visual-research loop. For the full endpoint catalog and response details, see the Pinterest public data API hub. When you are ready: