Pinterest Followers and Following API: Username or User ID?

Published October 20, 2026

userid=marthastewart is the correct starting input for a following request, but not for a follower request. Changing only the operation while leaving that field untouched changes a meaningful username into the wrong kind of identifier. The parameter name cannot diagnose its own value; the route must decide.

The observed Martha Stewart account makes the distinction concrete: username marthastewart, numeric user ID 32088353503445352. Keep both values, with different names in your application, before requesting either relationship collection.

/followers needs a numeric userid; /followings needs a username

The follower route lists accounts that follow the target; the following route lists accounts the target follows. Their public platform operations are list-user-followers and list-user-following. Both accept a query key called userid, but their documented starting requests differ:

list-user-followers  userid="32088353503445352"
list-user-following  userid="marthastewart"

Diagnose that one field before changing pagination, retrying or replacing the API key. A display name such as “Martha Stewart” is neither value. Although userid is optional in both parameter contracts, these starting workflows supply it; optional does not make the two identifier types interchangeable.

Platform requests use https://api.opendata-api.com/api/project/pinterest-scraper/ followed by the operation ID, with key=YOUR_API_KEY and endpoint parameters in the query string. Use your authorized platform key from the dashboard, not a Pinterest password or session. Keep it server-side and redact request URLs from logs and support messages. Opening documentation or copying examples sends no data request.

Resolve marthastewart once, then preserve 32088353503445352 losslessly

get-user-details accepts the account username and returns public profile metadata when available. If you only have a display name, use search-users to inspect candidates, then confirm a selected username separately. The observed search for martha stewart returned multiple accounts; its first match is not an identity guarantee.

The saved successful profile returned data.user.id as the quoted decimal string "32088353503445352". Preserve those exact digits as a string rather than converting the ID to a number. Its numeric magnitude exceeds JavaScript's safe-integer range. If another response encodes an ID as an unquoted JSON integer, parse its raw numeric token losslessly before string conversion: String() or BigInt() cannot repair an already-rounded number. Ordinary response.json() preserves quoted string IDs but cannot safely preserve every unquoted integer ID.

Python's standard JSON decoder preserves this quoted ID as a string and retains unquoted integer tokens as arbitrary-precision integers. This example checks the gateway envelope and profile identity before issuing the two relationship requests. Set OPENDATA_API_KEY to your authorized platform key; running the script sends three GET requests, not an exhaustive export.

import json
import os
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen

BASE = "https://api.opendata-api.com/api/project/pinterest-scraper"
KEY = os.environ["OPENDATA_API_KEY"]


def read(operation, **params):
    query = urlencode({"key": KEY, **params})
    request = Request(f"{BASE}/{operation}?{query}",
                      headers={"Accept": "application/json"})
    try:
        with urlopen(request, timeout=30) as response:
            envelope = json.load(response)
    except HTTPError as exc:
        raise RuntimeError(f"HTTP {exc.code}; inspect a redacted response") from None
    except URLError:
        raise RuntimeError("Transport failure; do not log the request URL") from None
    if not isinstance(envelope, dict) or str(envelope.get("code")) != "200":
        raise RuntimeError("Gateway business failure; inspect its redacted message")
    payload = envelope.get("data")
    if not isinstance(payload, dict) or payload.get("status") == "error":
        raise RuntimeError("Operation returned an error or unusable payload")
    operation_data = payload.get("data")
    if isinstance(operation_data, dict) and operation_data.get("status") == "error":
        raise RuntimeError("Operation returned a nested business error")
    return payload


username = "marthastewart"
profile = read("get-user-details", username=username)
profile_data = profile.get("data")
user = profile_data.get("user") if isinstance(profile_data, dict) else None
if (profile.get("status") != "ok" or not isinstance(user, dict)
        or user.get("username") != username):
    raise RuntimeError("Profile missing or identity not confirmed")
raw_id = user.get("id")
if isinstance(raw_id, bool) or not isinstance(raw_id, (str, int)):
    raise RuntimeError("Expected an exact string or integer account ID")
numeric_user_id = str(raw_id)
if not numeric_user_id.isascii() or not numeric_user_id.isdigit():
    raise RuntimeError("Expected a numeric account ID")

followers_reply = read("list-user-followers", userid=numeric_user_id)
following_reply = read("list-user-following", userid=username)

Store the resulting ID in a string column and preserve it when exporting to spreadsheets. Do not route it through a floating-point column or let spreadsheet software rewrite it in scientific notation.

Unwrap data.followers and data.followings as separate node collections

There are two layers to track: the gateway's code, message, data envelope and the operation payload inside it. The helper returns that operation payload. Its own data.followers or data.followings contains relationship records, not a flat profile array.

Observed follower records wrapped profile fields in node, including username, fullName, entityId and id, alongside a record-level cursor. Following records also used node, with confirmed username and id. Do not assume every optional profile field occurs in both collections or that entityId, id and continuation node_id are interchangeable.

def nodes(reply, collection):
    data = reply.get("data")
    rows = data.get(collection) if isinstance(data, dict) else None
    if not isinstance(rows, list):
        raise RuntimeError(f"Missing {collection} collection; not an empty result")
    if any(not isinstance(row, dict) or not isinstance(row.get("node"), dict)
           for row in rows):
        raise RuntimeError("Unexpected relationship record shape")
    return [row["node"] for row in rows]


follower_nodes = nodes(followers_reply, "followers")
following_nodes = nodes(following_reply, "followings")

For stored edges, keep the confirmed target ID, direction, returned node identifiers and your collection timestamp. APIs for similar apps can be requested via the contact page, and private source-code deployment is available.

A null follower cursor stops the export; a following bookmark is untested continuation

The observed follower response contained ten records, each with cursor: null. No usable continuation pair was available. Stop there: null is not a token, and a follower's profile ID is not an invented node_id.

The follower contract documents continuation with a response-provided node_id and cursor pair. On such a request, omit userid, including any form or client default, because it takes precedence. Do not merely append continuation fields to an unchanged initial query.

The following observation differed: fifty records had null record cursors, but the collection included a nonempty data.bookmark. Its documented continuation is to retain userid=marthastewart and send that exact returned token as bookmark. Another page was not requested, so token availability is evidence of a possible handoff, not verified pagination. Stop on absent or repeated tokens, deduplicate returned IDs, and retain the last confirmed checkpoint rather than claiming completion.

An HTTP 200 profile miss is not a zero-follower account

An observed deliberately nonexistent username returned HTTP 200 with operation status: error, an explanation that no data was found and data.user: {}. HTTP success therefore does not establish that a username exists. Even after gateway code indicates success, inspect the operation's status, message and actual profile content.

Do not default a missing follower_count to zero, insert an empty user as a confirmed account or interpret a missing relationship collection as an empty list. Keep separate outcomes for a usable profile, an explicit empty collection, unavailable profile data and a failed request. Only a returned numeric count of zero supports a zero-count observation.

This username-based public-data product is not the official Pinterest account-management API. Official account endpoints are authorization-associated and do not document an arbitrary-public-username parameter for these lookups. Do not transfer their scopes or pagination assumptions to this project's contracts.

Ten returned follower rows cannot stand in for 1,867,912 followers

The observed profile reported follower_count: 1867912 and following_count: 321; the relationship requests returned ten follower rows and fifty following rows. Counts describe profile metadata, while rows describe what those particular requests returned. Neither a count nor a bookmark proves that every relationship can be enumerated, and the returned rows are not a documented representative sample.

You can serialize collected rows to CSV yourself, but label the file as observed records, retaining direction, timestamp and stopping reason. Do not label it “all followers” or infer audience demographics, impressions, clicks or endorsement from those records.

The same boundary applies to pins: the observed username request returned 25 pin records, but list-user-pins exposes no pagination input. It cannot establish retrieval of every pin, and saved content does not prove original authorship or grant reuse rights. Keep the profile totals beside the observed row counts, not in place of them: the honest deliverable is a timestamped, scope-labeled collection whose missing coverage remains visible.