Blog/Developer Tools/

Threads Creator Research API Tutorial

Build a Threads creator-research workflow: resolve a list of handles into profile records, rank by reach, and extract bio links — one SandBase key, no login.

Dark cinematic render of a list of Threads handles resolving into ranked profile cards feeding an agent core

Creator research on Threads starts with a list of handles and ends with a ranked table you can act on: who has reach, who is verified, and where their audience goes next. This tutorial builds that workflow with the SandBase Threads API — resolve each handle to a public profile, rank by follower count, and pull the bio links. One SandBase key, no Threads login and no SDK.

For the full endpoint tour, see the Threads public data API hub. This piece is the applied research workflow.

Key takeaway

  • One endpoint does the work: threads/web/user-info with a username, called once per handle.
  • Every call is POST /v1/api/threads/<path> with one SANDBASE_API_KEY; responses share the same envelope — a completed run carries outputs, while a failed or timeout run carries error and no outputs.
  • The profile nests under a user key with fields like full_name, follower_count, is_verified, and bio_links — read them defensively.
  • Public, read-only data only; no Threads login of your own, but a SandBase API key is still required.

The workflow at a glance

StepEndpointInputYou get
1. Resolve profilesthreads/web/user-infousernamea profile record under user
2. Rank & extract(in your code)the recordsa table ranked by follower count, with bio links

SandBase Threads endpoint reference showing the user-info endpoint used in this workflow The endpoint API reference is the source of truth for each parameter name and response path.

Step 1 — Resolve each handle to a profile

Start with a shared helper that branches on status, then read one profile. Because a single handle can occasionally come back empty from the upstream, read defensively and skip anything without a user:

import os
import requests

BASE = "https://api.sandbase.ai/v1/api/threads"
HEADERS = {
    "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
    "Content-Type": "application/json",
}


def call(path: str, payload: dict) -> dict:
    resp = requests.post(f"{BASE}/{path}", headers=HEADERS, json=payload, timeout=90)
    resp.raise_for_status()
    body = resp.json()
    if body.get("status") != "completed":
        raise RuntimeError(body.get("error", {}).get("message", "request did not complete"))
    # The reference guarantees only the envelope; business fields vary by
    # endpoint, so read defensively and confirm against a live response.
    return body["outputs"][0]["data"]


def profile(username: str) -> dict | None:
    data = call("web/user-info", {"username": username})
    return data.get("user")  # may be absent for an occasional upstream miss


p = profile("zuck")
if p:
    print(p.get("full_name"), p.get("follower_count"), p.get("is_verified"))

Only the envelope is guaranteed, so the code reads each business field with .get(): a completed run carries outputs, while a failed or timeout run carries error and no outputs. The block below is my tested response, tested on 2026-09-27 (UTC) — the numbers will change, so treat them as a point-in-time reading and confirm against a live response:

{
  "id": "77059cb9-8616-4be1-abfe-96e82a12b54f",
  "status": "completed",
  "model": "threads/web/user-info",
  "outputs": [
    {
      "data": {
        "user": {
          "full_name": "Mark Zuckerberg",
          "biography": "Mostly superintelligence and MMA takes",
          "follower_count": 5744972,
          "is_verified": true,
          "bio_links": [],
          "id": "63055343223",
          "pk": "63055343223"
        }
      }
    }
  ]
}

SandBase Threads user-info API reference showing the username parameter and response schema user-info returns the profile under a user key — read the path defensively.

Step 2 — Resolve a list of handles

Loop your candidate handles through the same helper, skipping any that come back empty:

HANDLES = ["zuck", "mosseri", "natgeo"]

records = []
for handle in HANDLES:
    p = profile(handle)
    if not p:
        continue  # skip an occasional empty upstream result
    records.append({
        "username": handle,
        "name": p.get("full_name"),
        "followers": p.get("follower_count") or 0,
        "verified": p.get("is_verified", False),
        "bio": p.get("biography"),
        "links": [l.get("url") for l in (p.get("bio_links") or []) if isinstance(l, dict)],
    })

Each record is a flat, storable row. The bio_links extraction reads each link defensively, since the list can be empty and its item shape can vary — confirm it against a live response.

SandBase Threads endpoint list showing profile and related endpoints with their paths Read each endpoint’s schema; profile fields nest under user and can change over time.

Step 3 — Rank and report

With flat records in hand, ranking is a sort. Order by follower count and surface the verified accounts first for a quick research table:

def report(records: list[dict]) -> list[dict]:
    return sorted(
        records,
        key=lambda r: (r["verified"], r["followers"]),
        reverse=True,
    )


for r in report(records):
    badge = "✓" if r["verified"] else " "
    print(f"{badge} {r['followers']:>12,}  @{r['username']}  {r['name']}")

That is the whole loop: resolve, collect, rank. Because the records are flat JSON with named fields, the same table drops into a spreadsheet, a database, or an agent’s context without reshaping.

Putting it together

A minimal creator-research pass looks like this:

HANDLES = ["zuck", "mosseri", "natgeo"]

records = []
for handle in HANDLES:
    p = profile(handle)
    if p:
        records.append({
            "username": handle,
            "name": p.get("full_name"),
            "followers": p.get("follower_count") or 0,
            "verified": p.get("is_verified", False),
            "links": [l.get("url") for l in (p.get("bio_links") or []) if isinstance(l, dict)],
        })

ranked = report(records)

Because every call shares the same envelope and the same call helper, adding retries or rate-limit backoff is a one-place change. When you need more than the profile read — a user’s posts or replies — note that those endpoints key off a user_id, not a username: take the pk/id that user-info already returns and pass it as user_id (with an optional end_cursor for paging) to user-posts or user-replies. Check the live Threads listing and confirm the endpoint’s availability against its reference before wiring it in, since some post and search reads can be intermittent.

Why run this at the API layer

You could open each profile in a browser and copy the numbers, but that does not scale past a handful of handles and it gives you no structured data to rank. Reading through one uniform API means every handle returns the same envelope — a completed run carries outputs with a profile under user, while a failed or timeout run carries error and no outputs — so your loop is a few lines and your records are consistent. Auth is one key, retries live in one helper, and the ranking logic is a plain sort over flat rows.

That uniformity is what makes the workflow composable. Grow the handle list, add a field to each record, or feed the ranked table into a scoring step — the resolve loop does not change. Your time goes to what the profiles mean for your research, not to scraping and reshaping.

Limitations and boundaries

  • Public, read-only data only. No posting, following, or private/account-only data.
  • Read defensively. The profile nests under user, and an occasional handle can come back empty upstream — skip empties and use .get().
  • Parameters and shapes follow the upstream surface. user-info takes a username; bio_links may be empty. Inspect a real response and read the schema first.
  • Rate and volume. Treat responses as best-effort reads; retry with backoff on transient errors such as HTTP 429, and pace your loop.
  • Verify against the live reference. Availability and fields can change, and some post/search reads can be intermittent; confirm before building on a specific endpoint.

FAQ

Do I need a Threads or Meta login? No. You authenticate to SandBase with your SANDBASE_API_KEY. This workflow reads public profile data and needs no Threads account or OAuth on your side.

Why check for an empty result per handle? An individual user-info read can occasionally come back without a user object from the upstream. Reading data.get("user") and skipping empties keeps a batch loop robust rather than crashing on one miss.

Can I read private or account-only data? No. This workflow is public profile data only — name, bio, follower count, verified status, and bio links. Private and account-authorized content are out of scope.

Build it

Create a SandBase API key, resolve a list of handles, and rank them by reach. When you are ready: