Get Instagram Profile Data with an API in Python (Followers, Bio, Posts)

If you've ever tried to pull a public Instagram profile programmatically, you know the pain. The official Meta Graph API only works for accounts you own (or business accounts you manage through the platform), and rolling your own scraper means fighting login walls, rotating proxies, and parsing brittle HTML that breaks every few weeks.

This tutorial takes a different route. We'll use the Instagram Cheapest API on RapidAPI to fetch a public profile — follower count, bio, and metadata — and then pull that same user's recent posts. Everything is plain Python with requests, real-time JSON, and no scraping infrastructure to maintain.

By the end you'll have a small, reusable client that:

Prerequisites

You'll need:

Once subscribed, grab your x-rapidapi-key from the RapidAPI dashboard. Every request needs two headers:

Step 1: Set up the client

Let's start with the connection boilerplate. The base URL is https://instagram-cheapest.p.rapidapi.com and all endpoints are GET requests under the /api/v1/instagram path.

import requests

API_KEY = "YOUR_RAPIDAPI_KEY"  # keep this in an env var in production
HOST = "instagram-cheapest.p.rapidapi.com"
BASE_URL = f"https://{HOST}/api/v1/instagram"

HEADERS = {
    "x-rapidapi-key": API_KEY,
    "x-rapidapi-host": HOST,
}


def get(path, params=None):
    """Thin GET wrapper with basic error handling."""
    url = f"{BASE_URL}/{path}"
    resp = requests.get(url, headers=HEADERS, params=params, timeout=30)
    resp.raise_for_status()  # raises on 4xx/5xx
    return resp.json()

A note on the timeout: this API returns real-time, uncached data straight from Instagram, so responses are not instant. Give each call a generous timeout (30 seconds here) rather than letting it hang or fail prematurely.

Step 2: Fetch the profile with userinfo

The userinfo endpoint takes the username as a path parameter — it's the one endpoint that doesn't use a query string. The path is /api/v1/instagram/user/{username}.

def get_profile(username):
    return get(f"user/{username}")


profile = get_profile("nike")
print(profile)

Print the raw response first. The API returns raw JSON — exactly what Instagram returns — so the cleanest way to learn the shape is to inspect it directly rather than trust a field name you read in a blog post (including this one). Pretty-print it so you can scan the keys:

import json
print(json.dumps(profile, indent=2)[:2000])

Look through the output for the values you care about. A public profile payload like this typically includes the account's numeric user ID, the follower and following counts, the full name, the biography text, and whether the account is verified or private. The exact key names and nesting are whatever Instagram itself uses, so confirm them against your printed output before hardcoding them.

Once you've identified the keys, pull them out defensively with .get() so a missing field returns None instead of throwing a KeyError:

def extract_summary(profile):
    # Adjust these keys to match what you actually see in the printed JSON.
    return {
        "user_id": profile.get("id") or profile.get("pk"),
        "username": profile.get("username"),
        "full_name": profile.get("full_name"),
        "followers": profile.get("follower_count"),
        "bio": profile.get("biography"),
    }

This is the honest way to work with any third-party JSON API: read the real response, then map fields. Don't assume.

Step 3: Get the numeric user_id

Here's the key thing that trips people up: the posts and Reels endpoints don't accept a username — they need the numeric user_id. So your workflow is almost always two-step: resolve username → ID, then use the ID everywhere else.

You usually already have the ID from the userinfo response in Step 2. But if you only have a numeric UID and need to go the other way — ID back to username — the API gives you username_by_uid:

def get_username(uid):
    return get("username_by_uid", params={"uid": uid})


# Example: resolve a username from a numeric UID
print(get_username("13460080"))

There's also user_by_user_id if you want the full profile starting from an ID instead of a username:

def get_profile_by_id(user_id):
    return get("user_by_user_id", params={"user_id": user_id})

For our flow, we'll just reuse the ID we already extracted from the profile.

Step 4: Fetch the user's posts with user_media

Now we use the numeric ID with user_media, which takes user_id as a query parameter:

def get_user_media(user_id):
    return get("user_media", params={"user_id": user_id})


summary = extract_summary(profile)
user_id = summary["user_id"]

media = get_user_media(user_id)
print(json.dumps(media, indent=2)[:2000])

Again, inspect the structure before iterating. Media responses are typically a list of post objects (sometimes nested under a key like items or data). Each post object generally carries things like a shortcode, caption, media type (image/video/carousel), and per-post engagement counts. Iterate generically and guard each field:

def iter_posts(media_response):
    # The list might be at the top level or under a key — check your output.
    items = media_response if isinstance(media_response, list) else (
        media_response.get("items") or media_response.get("data") or []
    )
    for post in items:
        yield {
            "code": post.get("code") or post.get("shortcode"),
            "caption": post.get("caption"),
            "likes": post.get("like_count"),
            "comments": post.get("comment_count"),
        }


for post in iter_posts(media):
    print(post)

Notice the pattern: we never assume the response shape is fixed. We check whether it's a list or a dict, fall back across likely container keys, and use .get() for every field.

Step 5: Handle errors properly

A production client needs to handle the failures you'll actually hit: a missing username (404), an expired or wrong key (401/403), or rate limiting (429). Wrap the call and branch on the status code:

def safe_get(path, params=None):
    try:
        resp = requests.get(f"{BASE_URL}/{path}", headers=HEADERS,
                            params=params, timeout=30)
        resp.raise_for_status()
        return resp.json()
    except requests.exceptions.HTTPError as e:
        status = e.response.status_code
        if status == 404:
            print(f"Not found: {path}")
        elif status in (401, 403):
            print("Auth problem — check your x-rapidapi-key.")
        elif status == 429:
            print("Rate limited — slow down or upgrade your tier.")
        else:
            print(f"HTTP {status}: {e}")
    except requests.exceptions.RequestException as e:
        print(f"Network/timeout error: {e}")
    return None

The 429 case matters because each plan has a rate limit — the free Basic tier is capped at 1 request/second, so add a small delay if you're looping over many usernames.

Bandwidth tip: use the fields parameter

Every endpoint supports an optional fields parameter that lets you request only the JSON fields you actually need. If you're building a "followers count" widget, you don't need the full bio, profile-pic URLs, and every nested object on every call — that's wasted bandwidth.

Each tier includes 10 GB/month, after which you pay $0.001/MB. Trimming responses with fields keeps you comfortably inside that allowance:

# Conceptually: ask for a narrower payload to cut response size.
profile = get("user/nike", params={"fields": "username,follower_count"})

The provider has a dedicated tutorial on this ("How to use the fields parameter to reduce bandwidth usage"). When you're enriching thousands of profiles, narrowing the payload is the single easiest cost lever you have.

A quick word on cost

This API is built around being cheap at scale. Pricing is per request, and the per-1,000 rate drops as you move up tiers: from $0.10 per 1,000 requests on the top (Mega) tier, $0.11 on Ultra, and $0.13 on Pro. The Basic plan is free with 30 calls/month so you can prototype this whole tutorial without paying anything. Combined with the fields trick to stay under the 10 GB bandwidth allowance, profile enrichment pipelines stay genuinely inexpensive.

Conclusion

You now have a clean, two-step pattern for pulling public Instagram profile data in Python: resolve the username to a profile with userinfo, grab the numeric user_id, then fan out to user_media (and other ID-based endpoints) from there. Because the API returns raw, real-time JSON, the golden rule is to print the response and map fields from what you actually see — never from assumptions.

Ready to build? Grab your key and start with the free tier:

Get started with the Instagram Cheapest API on RapidAPI →

Compliance note: this API returns public Instagram data only. You're responsible for complying with Instagram's terms and applicable privacy law (GDPR/CCPA). It is not affiliated with or endorsed by Meta/Instagram.

Start Building Today

Get 30 free requests per month on the Basic plan. No commitment required.

Get Started on RapidAPI