An agency vetting creators, a sales team qualifying brands, an analyst tracking competitors: they all end up with the same spreadsheet, a column of Instagram handles that needs a column of follower counts next to it. One lookup is easy. A few hundred, with typos, renamed accounts and the odd slow response, is where scripts fall over.
This guide builds a script that reads handles from a CSV, or finds them with a keyword search, looks each one up within your plan's concurrency limit, and appends a dated row per account to a history file so the next run can show growth. Every response and output below is real, collected on October 6, 2026.
Before you start
- A ScrapingBot API key. Create a free account for 100 credits, enough for 20 accounts. No card needed.
- Python 3.9 or newer and
pip install requests. - A list of public Instagram accounts: handles,
@handlesor profile links all work.
One account: /user/by_username
Every Instagram call is a POST to https://scrapingbot.io/api/v1/instagram with the endpoint and its parameters in the body:
curl -X POST https://scrapingbot.io/api/v1/instagram \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endpoint": "/user/by_username", "params": {"username": "nasa"}}'
The profile comes back in data, with several dozen keys. These are the ones a follower report needs:
{
"success": true,
"data": {
"id": "528817151",
"username": "nasa",
"full_name": "NASA",
"follower_count": 104284782,
"following_count": 90,
"media_count": null,
"is_verified": true,
"is_private": false,
"is_business": false,
"category": "",
"external_url": "https://www.nasa.gov",
"biography": "Making the seemingly impossible, possible. ✨",
"bio_links": [ { "title": "NASA.gov Homepage", "url": "https://www.nasa.gov", … }, … ],
…
},
"duration": "0.26",
"statusCode": 200,
"creditsUsed": 5
}
| Field | In our 14 lookups |
|---|---|
follower_count, following_count | Always present, and exact rather than rounded. |
id | Always present. The numeric account ID stays the same when the handle changes, so key your records on it. |
is_verified, is_private | Present: the verified badge and whether the account is private. |
category | The label under the name, such as "Coffee shop" or "Nonprofit organization". Set on 7 of the 10 coffee accounts below, empty for NASA, National Geographic and Nike. |
external_url | The main website link, when the account has one. |
media_count | null on all 14. Do not depend on the post count. |
is_business | false on all 14, including Nike and five cafés. Do not use it to tell brands from people; category is the better signal. |
What goes wrong in a long list
Run a few hundred handles and you will meet three kinds of failure. They need different handling:
| Status | Error | What to do |
|---|---|---|
404 | Instagram user was not found. | A typo, a deleted account or a rename. Retrying will not help: mark the row and move on. |
408 | We could not process your request in time. Please try again. | Instagram was slow to answer. Retry after a short pause; it usually works. |
429 | Concurrency limit reached. Your plan allows 1 concurrent request(s). | You sent more at once than your plan allows (the number is your plan's limit). Wait and retry, and lower the concurrency. |
None of these are charged: failed calls are refunded. Response times also vary a lot. The 14 lookups for this guide took between 0.29 and 6 seconds each, so give every request a generous timeout and let a few run in parallel instead of shortening it.
The script
Save this as ig_followers.py. It takes a CSV of handles (or --search and a keyword), cleans and de-duplicates the handles, looks them up in parallel up to CONCURRENCY, prints a report sorted by followers, and appends every row, found or not, to instagram_followers.csv.
import csv
import os
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import date
import requests
API = "https://scrapingbot.io/api/v1/instagram"
HEADERS = {"x-api-key": "YOUR_API_KEY"}
CONCURRENCY = 1 # your plan's limit: Free 1, Starter 10, Startup 50
HISTORY = "instagram_followers.csv"
FIELDS = ["date", "username", "user_id", "name", "followers", "following",
"verified", "private", "category", "website", "status"]
credits = {"used": 0}
lock = threading.Lock()
class NotFound(Exception):
pass
def call(endpoint, params, tries=4):
"""One Instagram call: 5 credits on success. Failed calls are refunded."""
for attempt in range(tries):
res = requests.post(API, headers=HEADERS, timeout=60,
json={"endpoint": endpoint, "params": params})
body = res.json()
if body.get("success"):
with lock:
credits["used"] += body.get("creditsUsed", 0)
return body["data"]
if res.status_code == 404: # no such account: retrying will not help
raise NotFound(body.get("error"))
if res.status_code in (401, 402): # bad key or out of credits: stop the batch
sys.exit(body.get("error"))
if res.status_code == 400:
raise RuntimeError(body.get("error"))
time.sleep(2 ** attempt) # 408, 429 or 5xx: wait and try again
raise RuntimeError(f"kept failing: {body.get('error')}")
def clean(handle):
"""'@NASA', 'nasa' and 'https://www.instagram.com/nasa/' all become 'nasa'."""
h = handle.strip().lstrip("@")
if "instagram.com/" in h:
h = h.split("instagram.com/")[1].split("/")[0].split("?")[0]
return h.lower()
def lookup(username):
row = {"date": date.today().isoformat(), "username": username}
try:
p = call("/user/by_username", {"username": username})
except NotFound:
return {**row, "status": "not found"}
except RuntimeError as err:
return {**row, "status": f"error: {err}"}
return {**row, "status": "ok", "user_id": p["id"], "name": p.get("full_name") or "",
"followers": p.get("follower_count"), "following": p.get("following_count"),
"verified": p.get("is_verified"), "private": p.get("is_private"),
"category": p.get("category") or "", "website": p.get("external_url") or ""}
def read_handles(path):
with open(path, newline="", encoding="utf-8") as f:
if path.endswith(".csv"):
reader = csv.DictReader(f)
col = "username" if "username" in reader.fieldnames else reader.fieldnames[0]
return [r[col] for r in reader if r[col].strip()]
return [line for line in f if line.strip()]
def search_handles(keyword, limit):
data = call("/search/users_by_keyword", {"keyword": keyword})
return [u["user"]["username"] for u in data.get("users", [])][:limit]
if __name__ == "__main__":
if sys.argv[1] == "--search":
handles = search_handles(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 10)
else:
handles = read_handles(sys.argv[1])
handles = list(dict.fromkeys(clean(h) for h in handles)) # drop repeats, keep the order
with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
rows = list(pool.map(lookup, handles))
found = sorted((r for r in rows if r["status"] == "ok"),
key=lambda r: r["followers"] or 0, reverse=True)
for r in found:
print(f"@{r['username']:<28} {r['followers']:>12,} followers {r['category']}")
for r in rows:
if r["status"] != "ok":
print(f"@{r['username']}: {r['status']}")
new_file = not os.path.exists(HISTORY)
with open(HISTORY, "a", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
if new_file:
writer.writeheader()
writer.writerows(rows)
print(f"{len(found)} of {len(handles)} found, {credits['used']} credits; added to {HISTORY}")
- Handles are cleaned before anything is spent.
clean()strips@, pulls the handle out of profile links and lowercases it, anddict.fromkeysdrops repeats. Spreadsheets collected by hand always have both "@NASA" and "nasa". - Each failure is handled once, in
call(). A404becomes a "not found" row; a408,429or server error is retried with a growing pause; a bad key or an empty balance stops the whole run instead of failing every row the same way. - Set
CONCURRENCYto your plan's limit. Free allows 1 request in flight, Starter 10 and Startup 50 (the concurrency table lists every plan). With slow responses of several seconds, parallel requests are what make a list of thousands finish in minutes. - Credits come from the responses. Each successful call reports
creditsUsed, and refunded failures add nothing, so the total printed at the end is what the run cost. - The history file only grows. Every run appends rows with today's date, so the file becomes a time series without a database.
Run it on a CSV
A typical hand-made list, with a profile link, a repeat in different case and a handle that does not exist:
username,notes
@NASA,space
natgeo,
https://www.instagram.com/nike/,
patagonia,
nasa,dup
zzzz_no_such_user_8812734,
python ig_followers.py brands.csv
@nike 291,041,708 followers
@natgeo 268,435,354 followers
@nasa 104,275,906 followers
@patagonia 5,516,763 followers
@zzzz_no_such_user_8812734: not found
4 of 5 found, 20 credits; added to instagram_followers.csv
Six rows became five lookups, the missing account cost nothing, and the four that exist cost 5 credits each. The rows in instagram_followers.csv:
date,username,user_id,name,followers,following,verified,private,category,website,status
2026-10-06,nasa,528817151,NASA,104275906,90,True,False,,https://www.nasa.gov,ok
2026-10-06,natgeo,787132,National Geographic,268435354,194,True,False,,http://visitstore.bio/natgeo,ok
2026-10-06,nike,13460080,Nike,291041708,266,True,False,,http://empli.fi/nike,ok
2026-10-06,patagonia,143939018,Patagonia,5516763,888,True,False,,http://sprout.link/patagonia/,ok
2026-10-06,zzzz_no_such_user_8812734,,,,,,,,,not found
No list yet? Start from a keyword
/search/users_by_keyword returns the accounts Instagram suggests for a keyword. One call (5 credits) for "specialty coffee" returned 45 accounts. Each result has the handle, display name, verified badge and numeric ID, but no follower count, so the script looks each one up afterwards:
{
"success": true,
"data": {
"status": "ok",
"query": "specialty coffee",
"users": [
{ "position": 0, "user": { "id": "77451051844", "username": "q.specialtycoffee", "full_name": "Q Specialty Coffee", "is_verified": true, … } },
{ "position": 1, "user": { "id": "6329294631", "username": "v60official", "full_name": "Specialty Coffee", "is_verified": false, … } },
…
]
},
"creditsUsed": 5
}
python ig_followers.py --search "specialty coffee" 10
@specialtycoffeeassociation 350,321 followers Nonprofit organization
@q.specialtycoffee 11,369 followers
@v60official 10,417 followers Coffee shop
@roaster_specialtycoffee 8,927 followers
@blck.specialtycoffee 3,691 followers Coffee shop
@xwave_specialtycoffee 3,038 followers Coffee shop
@bnkr_coffee_georgia 2,444 followers Coffee shop
@specialtycoffeefactoryoutlet 1,689 followers
@roundcoffee.bar 1,218 followers Cafe
@fiandacoffee_hq 1,184 followers Restaurant
10 of 10 found, 55 credits; added to instagram_followers.csv
Search order is not follower order. The Specialty Coffee Association was the tenth suggestion and has 30 times the followers of the first. If you want the biggest accounts for a keyword, look up more of the results than you need and sort, as the script does. The run cost 55 credits: 5 for the search and 5 for each of the 10 profiles.
Follower growth from the history file
Instagram's counts are exact, so even a large account's change from one day to the next is visible. NASA's count was 104,285,314 when we looked on October 3, 104,284,782 at 05:40 UTC on October 6, and 104,275,906 that afternoon: 8,876 fewer within the same day. (TikTok, by contrast, rounds large counts, so small changes on big accounts do not show there.)
Run the script on the same list every day or week, then compare each account's two latest snapshots:
import csv
from collections import defaultdict
history = defaultdict(dict) # username -> {date: followers}
with open("instagram_followers.csv", newline="", encoding="utf-8") as f:
for r in csv.DictReader(f):
if r["status"] == "ok":
history[r["username"]][r["date"]] = int(r["followers"])
for user, by_date in sorted(history.items()):
days = sorted(by_date)
if len(days) >= 2:
before, after = by_date[days[-2]], by_date[days[-1]]
print(f"@{user}: {before:,} -> {after:,} ({after - before:+,}) "
f"from {days[-2]} to {days[-1]}")
If an account was looked up twice on the same day, the later row wins. Accounts get renamed; for long-running tracking, key the history on user_id instead of the handle, and send the ID to /user/by_id once you have it.
Cost and speed
- 5 credits per account found, nothing for a failed lookup. 1,000 accounts is 5,000 credits, about $0.91 on the Starter plan ($49.99 for 275,000 credits, enough for 55,000 lookups a month).
- Speed comes from concurrency. Most lookups answer in under half a second, but some take several seconds. On Free (1 at a time) a list of 20 takes well under a minute; on Starter, 10 workers get through 1,000 handles in a few minutes.
- Searching adds 5 credits per keyword, whatever the number of results.
Where to go from here
- Just need a few numbers? The free Instagram profile stats tool shows one account's followers, recent likes and comments and an engagement rate, with no code and no sign-up.
- Go past the counts: fetch a profile's reels with likes and comments to see whether the audience still engages.
- Doing the same for TikTok: TikTok follower and like counts with Python.
- The profile reference and search reference list every parameter, and the Instagram API page lists every endpoint.
Common questions
Is there an API for Instagram follower counts?
Yes. POST {"endpoint": "/user/by_username", "params": {"username": "nasa"}} to https://scrapingbot.io/api/v1/instagram with your key in the x-api-key header and read data.follower_count. It works for any public account, needs no Instagram login and costs 5 credits per lookup.
How do I get follower counts for a list of Instagram accounts?
Look each handle up with /user/by_username, a few at a time within your plan's concurrency limit. The script in this guide reads handles from a CSV, cleans and de-duplicates them, retries timeouts, marks missing accounts and appends a dated row per account to a history file.
Are Instagram follower counts exact?
Yes. Unlike TikTok, which rounds large counts, follower_count is exact: NASA showed 104,284,782 followers at 05:40 UTC on October 6, 2026 and 104,275,906 that afternoon. That makes day-to-day growth measurable even for very large accounts.
Why is media_count null?
Instagram did not include the post count for any of the 14 accounts we looked up for this guide. Treat media_count as optional, and count posts by paging /medias/by_user_id if you need the number.
What does "Instagram user was not found" mean?
The handle does not exist (a typo, a deleted account or a rename). It comes back as 404, retrying will not help, and failed calls are not charged. Timeouts (408) are different: retry those.
How much does a bulk lookup cost?
5 credits per account found. 1,000 accounts is 5,000 credits, about $0.91 on the Starter plan ($49.99 for 275,000 credits). The 100 free credits cover 20 lookups.