Back to Blog

Threads API Python Tutorial: Get Threads Data in Minutes

· 8 min read

Why Python Developers Need Threads Data Access

Threads has over 320 million monthly active users and growing. For researchers, analysts, and developers, that represents a substantial dataset — brand sentiment, trend tracking, academic social media analysis, competitive intelligence.

The problem: the official Meta Threads API is locked behind OAuth approval, requires a business account, and imposes strict rate limits (250 posts/24 hours, 500 searches/7 days). For programmatic research or production tooling, those constraints make the official API impractical.

thredly provides a straightforward REST API that bypasses this friction. No OAuth dance, no business account approval, no scraping brittle HTML. Just authenticated HTTP requests returning clean JSON — designed specifically for threads api python workflows.

This tutorial covers everything from fetching a single profile to building an analytics script that aggregates engagement data across endpoints.

Prerequisites

Set your API key as an environment variable to avoid hardcoding credentials:

export RAPIDAPI_KEY="your_api_key_here"

Then load it in Python:

import os
import requests

API_KEY = os.environ.get("RAPIDAPI_KEY")
BASE_URL = "https://threads-api-pro.p.rapidapi.com"

HEADERS = {
    "X-RapidAPI-Key": API_KEY,
    "X-RapidAPI-Host": "threads-api-pro.p.rapidapi.com"
}

All code examples in this tutorial assume these variables are defined.

Fetch a User Profile

The profile endpoint returns core account metadata. This is typically the first call in any threads api python tutorial workflow — you confirm the user exists and retrieve baseline metrics before fetching content.

def get_user_profile(username: str) -> dict:
    url = f"{BASE_URL}/api/user/{username}"
    response = requests.get(url, headers=HEADERS)
    response.raise_for_status()
    return response.json()

profile = get_user_profile("zuck")
print(profile)

Response structure:

{
  "success": true,
  "data": {
    "username": "zuck",
    "full_name": "Mark Zuckerberg",
    "biography": "...",
    "follower_count": 3200000,
    "following_count": 500,
    "post_count": 847,
    "is_verified": true,
    "profile_pic_url": "https://..."
  }
}

Key fields:

  • follower_count / following_count — audience size and follow ratio
  • post_count — total posts published, useful for calculating posting frequency
  • is_verified — Meta verified status
  • biography — bio text for keyword or sentiment analysis

Get User Posts

Fetch recent posts for any public account. This endpoint supports cursor-based pagination, which is essential for how to get threads data with python at scale — iterating through hundreds of posts without missing any.

def get_user_posts(username: str, cursor: str = None) -> dict:
    url = f"{BASE_URL}/api/user/{username}/posts"
    params = {}
    if cursor:
        params["cursor"] = cursor
    response = requests.get(url, headers=HEADERS, params=params)
    response.raise_for_status()
    return response.json()

def get_all_posts(username: str) -> list:
    all_posts = []
    cursor = None

    while True:
        data = get_user_posts(username, cursor)
        posts = data.get("data", {}).get("posts", [])
        all_posts.extend(posts)

        # Check for next page
        cursor = data.get("data", {}).get("next_cursor")
        if not cursor:
            break

    return all_posts

posts = get_all_posts("zuck")
print(f"Fetched {len(posts)} posts")

Each post object includes:

{
  "id": "17841234567890",
  "text": "Post content here...",
  "timestamp": "2026-02-20T14:30:00Z",
  "like_count": 45200,
  "reply_count": 1840,
  "repost_count": 2100,
  "media_type": "text"
}

The next_cursor value is null when you have reached the last page. The loop exits cleanly without any additional state tracking.

Search Threads

The search endpoint enables threads data extraction python workflows that monitor keywords, hashtags, or brand mentions across the platform. This is useful for trend analysis, competitive monitoring, and academic research.

def search_users(keyword: str) -> dict:
    url = f"{BASE_URL}/api/search/users"
    params = {"q": keyword}
    response = requests.get(url, headers=HEADERS, params=params)
    response.raise_for_status()
    return response.json()

results = search_users("machine learning")
users = results.get("data", {}).get("users", [])

for user in users:
    print(f"{user['username']}{user['follower_count']} followers")

The search endpoint returns user accounts matching the query. Each result includes profile metadata, making it straightforward to build discovery pipelines — find relevant accounts, then feed those usernames into the profile and posts endpoints for deeper analysis.

Get Followers and Following

Fetch follower lists for audience research, influencer mapping, or community graph analysis. This is one of the most in-demand capabilities for get threads data programmatically use cases.

def get_followers(username: str, cursor: str = None) -> dict:
    url = f"{BASE_URL}/api/user/{username}/followers"
    params = {}
    if cursor:
        params["cursor"] = cursor
    response = requests.get(url, headers=HEADERS, params=params)
    response.raise_for_status()
    return response.json()

def get_all_followers(username: str) -> list:
    all_followers = []
    cursor = None

    while True:
        data = get_followers(username, cursor)
        followers = data.get("data", {}).get("users", [])
        all_followers.extend(followers)

        cursor = data.get("data", {}).get("next_cursor")
        if not cursor:
            break

    return all_followers

followers = get_all_followers("someuser")
print(f"Total followers fetched: {len(followers)}")

Each follower entry contains username, full_name, follower_count, and is_verified — enough data to segment audiences by account size or verification status without additional lookups.

Export Data to CSV and JSON

Raw API responses are useful for programmatic analysis, but researchers often need portable formats for spreadsheets, databases, or sharing with collaborators. Python’s standard library handles both CSV and JSON export with no additional dependencies.

Export to JSON

import json

def save_json(data: list, filename: str) -> None:
    with open(filename, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)
    print(f"Saved {len(data)} records to {filename}")

posts = get_all_posts("someuser")
save_json(posts, "threads_posts.json")

Export to CSV

import csv

def save_posts_csv(posts: list, filename: str) -> None:
    if not posts:
        print("No posts to save")
        return

    fieldnames = ["id", "text", "timestamp", "like_count", "reply_count", "repost_count"]

    with open(filename, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(posts)

    print(f"Saved {len(posts)} posts to {filename}")

save_posts_csv(posts, "threads_posts.csv")

The extrasaction="ignore" parameter drops any fields not in fieldnames, so the code stays stable even if the API adds new response fields.

Error Handling

Production-grade threads api python example code needs to handle network failures, invalid usernames, rate limit responses, and unexpected API errors. A consistent pattern prevents silent failures.

import time

def safe_request(url: str, params: dict = None, retries: int = 3) -> dict | None:
    for attempt in range(retries):
        try:
            response = requests.get(url, headers=HEADERS, params=params, timeout=10)

            if response.status_code == 404:
                print(f"Resource not found: {url}")
                return None

            if response.status_code == 429:
                wait_time = 2 ** attempt  # Exponential backoff: 1s, 2s, 4s
                print(f"Rate limited. Waiting {wait_time}s before retry {attempt + 1}/{retries}")
                time.sleep(wait_time)
                continue

            response.raise_for_status()
            return response.json()

        except requests.exceptions.Timeout:
            print(f"Request timed out (attempt {attempt + 1}/{retries})")
        except requests.exceptions.ConnectionError:
            print(f"Connection error (attempt {attempt + 1}/{retries})")
        except requests.exceptions.HTTPError as e:
            print(f"HTTP error: {e}")
            return None

    print(f"All {retries} retries failed for {url}")
    return None

Key behaviors in this pattern:

  • 404: Non-retryable. Username does not exist or account is private. Return None and continue.
  • 429: Rate limited. Exponential backoff before retry. Most rate limit windows reset within seconds.
  • Timeout: Retry with the same backoff logic.
  • HTTPError: Log and return None rather than crashing the entire pipeline.

Replace direct requests.get() calls with safe_request() in any long-running data collection script.

Build a Simple Analytics Script

Combining endpoints gives you a complete picture of any public Threads account. This script uses threads api no login access to calculate engagement rate, identify top-performing posts, and measure posting frequency.

import os
import json
import statistics
from datetime import datetime, timezone

def analyze_threads_account(username: str) -> dict:
    print(f"Analyzing @{username}...")

    # 1. Fetch profile
    profile_data = safe_request(f"{BASE_URL}/api/user/{username}")
    if not profile_data or not profile_data.get("success"):
        return {"error": f"Could not fetch profile for {username}"}

    profile = profile_data["data"]
    follower_count = profile.get("follower_count", 0)

    # 2. Fetch posts (first page only for quick analysis)
    posts_data = safe_request(f"{BASE_URL}/api/user/{username}/posts")
    posts = posts_data.get("data", {}).get("posts", []) if posts_data else []

    if not posts:
        return {"error": "No posts found"}

    # 3. Calculate engagement metrics
    engagement_scores = []
    for post in posts:
        likes = post.get("like_count", 0)
        replies = post.get("reply_count", 0)
        reposts = post.get("repost_count", 0)
        total_engagement = likes + replies + reposts

        if follower_count > 0:
            rate = (total_engagement / follower_count) * 100
            engagement_scores.append(rate)

    avg_engagement_rate = statistics.mean(engagement_scores) if engagement_scores else 0

    # 4. Find top post
    top_post = max(
        posts,
        key=lambda p: p.get("like_count", 0) + p.get("reply_count", 0) + p.get("repost_count", 0)
    )

    # 5. Calculate posting frequency
    timestamps = []
    for post in posts:
        ts = post.get("timestamp")
        if ts:
            timestamps.append(datetime.fromisoformat(ts.replace("Z", "+00:00")))

    posting_frequency = None
    if len(timestamps) >= 2:
        timestamps.sort()
        total_days = (timestamps[-1] - timestamps[0]).days or 1
        posting_frequency = round(len(timestamps) / total_days, 2)

    return {
        "username": username,
        "full_name": profile.get("full_name"),
        "follower_count": follower_count,
        "posts_analyzed": len(posts),
        "avg_engagement_rate_pct": round(avg_engagement_rate, 4),
        "top_post": {
            "id": top_post.get("id"),
            "text_preview": top_post.get("text", "")[:100],
            "like_count": top_post.get("like_count"),
            "reply_count": top_post.get("reply_count"),
        },
        "posts_per_day": posting_frequency,
    }


if __name__ == "__main__":
    result = analyze_threads_account("zuck")
    print(json.dumps(result, indent=2))

Sample output:

{
  "username": "zuck",
  "full_name": "Mark Zuckerberg",
  "follower_count": 3200000,
  "posts_analyzed": 20,
  "avg_engagement_rate_pct": 1.4832,
  "top_post": {
    "id": "17841234567890",
    "text_preview": "Excited to share what we've been building...",
    "like_count": 98400,
    "reply_count": 4210
  },
  "posts_per_day": 0.71
}

This output is ready to pipe into a database, a dashboard, or a research report without further transformation. Extend the script by calling get_all_posts() instead of the single-page fetch for complete historical analysis.

Next Steps

You now have a working Python foundation for scrape threads python-style workflows — without the fragility of actual scraping. The thredly API handles session management, rate limit rotation, and response normalization so your code stays clean.

Where to go from here:

For high-volume research or production pipelines, review the pricing page for rate limit tiers that match your request volume.