Guide · Python

Look up Instagram follower counts in bulk with Python

Turn a spreadsheet of handles, or a keyword, into follower counts for every account, without one bad handle or slow response stopping the run.

ScrapingBot 8 min read
Endpoint
/user/by_username
Cost
5 credits per account
Language
Python 3.9+
Needs
requests
On this page

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, @handles or 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
}
FieldIn our 14 lookups
follower_count, following_countAlways present, and exact rather than rounded.
idAlways present. The numeric account ID stays the same when the handle changes, so key your records on it.
is_verified, is_privatePresent: the verified badge and whether the account is private.
categoryThe 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_urlThe main website link, when the account has one.
media_countnull on all 14. Do not depend on the post count.
is_businessfalse 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:

StatusErrorWhat to do
404Instagram user was not found.A typo, a deleted account or a rename. Retrying will not help: mark the row and move on.
408We could not process your request in time. Please try again.Instagram was slow to answer. Retry after a short pause; it usually works.
429Concurrency 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, and dict.fromkeys drops repeats. Spreadsheets collected by hand always have both "@NASA" and "nasa".
  • Each failure is handled once, in call(). A 404 becomes a "not found" row; a 408, 429 or 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 CONCURRENCY to 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

/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

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.

Start scraping in the next five minutes.

100 free credits, no credit card. One API key works for websites, TikTok, Instagram, Google, Amazon and ChatGPT.