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:
- Looks up a profile by username
- Extracts the numeric
user_idfrom the profile - Uses that ID to fetch the user's media
- Handles errors and trims bandwidth with the
fieldsparameter
Prerequisites
You'll need:
- Python 3.8+ and the
requestslibrary (pip install requests) - A free RapidAPI account
- A subscription to the Instagram Cheapest API — the Basic plan is free and gives you 30 calls a month, which is plenty to follow along
Once subscribed, grab your x-rapidapi-key from the RapidAPI dashboard. Every request needs two headers:
x-rapidapi-key: your personal keyx-rapidapi-host:instagram-cheapest.p.rapidapi.com
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.