Guide · Python

Export Instagram followers and following to CSV with Python

Get the list behind a follower count: who follows an account, who it follows, and which accounts two creators have in common, saved to CSV.

ScrapingBot 11 min read
Endpoints
/followers/by_user_id, /following/by_user_id
Cost
5 credits per page of 50
Language
Python 3.9+
Needs
requests
On this page

A follower count says how big an audience is. The list behind it says who is in it: which brands and creators follow a café, which accounts a creator follows, which names two accounts in the same niche have in common. Instagram's own API doesn't give you that list, and on some accounts Instagram itself shows only part of it.

This guide exports an account's followers or following to CSV with Python, tells you when Instagram has cut the list short, checks whether one account follows another, and compares two lists. Every response and output below is real, collected through ScrapingBot on October 27, 2026.

Before you start

  • A ScrapingBot API key. Create a free account for 100 credits: 20 pages, up to about 1,000 accounts. No card needed.
  • Python 3.9 or newer and pip install requests.
  • A public Instagram account to start from. Private accounts' lists aren't available.

Meta's API gives counts, not lists

Meta's Instagram API exposes followers_count and follows_count on business and creator accounts, and its business_discovery edge reads those counts for other professional accounts. None of its edges lists the followers or followed accounts themselves (checked October 2026). The lists are visible only in Instagram itself, and that is what /followers/by_user_id and /following/by_user_id return as JSON.

One page of followers

Every Instagram call is a POST to https://scrapingbot.io/api/v1/instagram. Send the account in user_id: the numeric ID, or simply the username, which is looked up for you within the same call and charge.

curl -X POST https://scrapingbot.io/api/v1/instagram \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endpoint": "/followers/by_user_id", "params": {"user_id": "specialtycoffeeassociation"}}'

The first page of the Specialty Coffee Association's followers, trimmed to one of its 50 accounts:

{
  "success": true,
  "data": {
    "users": [
      {
        "id": "76612778675",
        "username": "crownandscale",
        "full_name": "Crown + Scale Collective",
        "is_private": false,
        "is_verified": false,
        "latest_reel_media": 0,
        "has_anonymous_profile_picture": false,
        "profile_pic_url": "https://instagram.fbhx2-2.fna.fbcdn.net/v/t51.82787-19/…",
        …
      },
      …
    ],
    "user_count": 352974,
    "page_size": 50,
    "has_more": true,
    "next_max_id": "275|QVFEcnNVNGIwaElVb2xzLWRKZGNGY3NBd3d5…",
    "should_limit_list_of_followers": false,
    "status": "ok",
    …
  },
  "duration": "1.83",
  "statusCode": 200,
  "creditsUsed": 5
}
FieldWhat it is
users[].idThe numeric account ID. It survives a change of username, so key your records on it.
username, full_nameThe handle and display name. full_name can be empty.
is_private, is_verifiedWhether the follower's own account is private, and whether it has the verified badge. 123 of the 420 followers in our first export were private.
latest_reel_mediaThe time of the account's latest story while one is up (a Unix timestamp), otherwise 0.
profile_pic_urlA signed image link that expires; download the picture soon if you need it.
user_countThe account's full follower count (following count on /following/by_user_id). Not the number of accounts on this page.
has_more, next_max_idWhether another page exists, and the cursor to send back as max_id to get it.

Each page holds up to 50 accounts and costs 5 credits; the follower objects carry no follower counts of their own. Look an account up with /user/by_username when you need its numbers.

What Instagram lets you see

We exported or sampled nine lists from eight public accounts for this guide. Most paged to the end; three follower lists stopped after one page, whatever their size suggested:

ListWhat came back
@crownandscale followers73 of 81, in 2 pages
@guakita_caffe followers420 of 422, in 9 pages
@cafecoadoclube followersPages; first page of 827 checked
@specialtycoffeeassociation followersPages; first two pages of 352,974 checked
@lancehedrick followers50 of 134,706, then has_more: false
@morgandrinkscoffee followers49 of 1,386,873, then has_more: false
@nasa followers50 of 104 million, then has_more: false
@lancehedrick following1,281 of 1,285, in 26 pages
@jimseven following1,122 of 1,118, in 23 pages
  • Some follower lists are cut short. Lance Hedrick has fewer followers than the Specialty Coffee Association, yet his list ended after 50 while the association's kept going. You can't predict it from the size; you can detect it: has_more is false while you hold far fewer accounts than user_count. Those short lists also changed from one call to the next.
  • Counts and lists rarely match exactly. Expect a few accounts fewer than user_count (420 of 422), occasionally a few more (1,122 of 1,118, as follows changed during the run), and now and then the same account on two pages, so de-duplicate on id.
  • Following lists paged to the end in every case we tried.
  • Private accounts return an empty list with no user_count, as a successful 5-credit call.

The script

Save this as ig_follows.py. It takes a username, pages through the followers (or the following with --following) up to --pages pages, de-duplicates on the account ID, writes a new timestamped CSV each run (never overwriting an earlier one) and says whether the list was complete, cut short by Instagram, or stopped early.

import csv
import os
import sys
import time
from datetime import datetime

import requests

API = "https://scrapingbot.io/api/v1/instagram"
HEADERS = {"x-api-key": "YOUR_API_KEY"}
FIELDS = ["user_id", "username", "full_name", "is_private", "is_verified", "has_story"]


class Stop(Exception):
    """Why paging stopped early. Whatever was collected is still saved."""


def call(endpoint, params, tries=4):
    """One Instagram call: 5 credits on success. Failed calls are refunded."""
    error = None
    for attempt in range(tries):
        try:
            res = requests.post(API, headers=HEADERS, timeout=60,
                                json={"endpoint": endpoint, "params": params})
            body = res.json()
        except (requests.RequestException, ValueError) as err:  # no answer, or not JSON
            error = err
        else:
            if body.get("success"):
                return body["data"]
            if res.status_code in (400, 401, 402, 404):  # retrying will not help
                raise Stop(f"{res.status_code} {body.get('error')}")
            error = body.get("error")
        time.sleep(2 ** attempt)  # 408, 429, 5xx or a network error: wait and try again
    raise Stop(f"kept failing: {error}")


def export(account, kind="followers", max_pages=20):
    """Page through an account's followers (or following), 50 accounts a page."""
    params = {"user_id": account}  # a username works; it is looked up for you
    rows, total, pages, end = {}, None, 0, "reached --pages; raise it for more"
    while pages < max_pages:
        try:
            data = call(f"/{kind}/by_user_id", params)
        except Stop as reason:
            end = str(reason)
            break
        pages += 1
        total = data.get("user_count", total)
        for u in data.get("users", []):
            rows.setdefault(u["id"], {  # pages can repeat an account: keep the first
                "user_id": u["id"], "username": u["username"],
                "full_name": u.get("full_name") or "",
                "is_private": u.get("is_private"), "is_verified": u.get("is_verified"),
                "has_story": bool(u.get("latest_reel_media")),
            })
        if not data.get("has_more") or not data.get("next_max_id"):
            end = "end of list"
            break
        params = {"user_id": account, "max_id": data["next_max_id"]}
    return list(rows.values()), total, pages, end


if __name__ == "__main__":
    account = sys.argv[1].lstrip("@")
    kind = "following" if "--following" in sys.argv else "followers"
    max_pages = int(sys.argv[sys.argv.index("--pages") + 1]) if "--pages" in sys.argv else 20

    rows, total, pages, end = export(account, kind, max_pages)
    if pages == 0:
        sys.exit(f"@{account}: {end}")
    if total is None and not rows:
        sys.exit(f"@{account} is private: Instagram lists {kind} of public accounts only.")
    total = len(rows) if total is None else total

    # One file per run, never overwritten: two exports are what you compare later.
    stamp = f"{account}_{kind}_{datetime.now():%Y%m%d-%H%M%S}"
    path, n = f"{stamp}.csv", 1
    while os.path.exists(path):
        path, n = f"{stamp}-{n}.csv", n + 1
    with open(path, "x", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        writer.writerows(rows)

    print(f"@{account}: {len(rows):,} of {total:,} {kind} in {pages} page{'s' if pages != 1 else ''} "
          f"({pages * 5} credits), saved to {path}")
    print(f"  {sum(r['is_private'] for r in rows)} private, "
          f"{sum(r['is_verified'] for r in rows)} verified, "
          f"{sum(r['has_story'] for r in rows)} with a story up")
    if end == "end of list" and len(rows) < 0.9 * total:
        print(f"  Instagram shows only {len(rows)} of this account's {kind}: "
              f"the list ends there, so no further pages exist.")
    elif end != "end of list":
        print(f"  Stopped early: {end}")
  • The cursor is opaque; pass it back as is. On following lists and small follower lists next_max_id looked like an offset ("50", "100"); on the Specialty Coffee Association's followers it was "275|QVFE…". Don't build it yourself.
  • --pages caps the spend. The default of 20 pages is 100 credits, the free allowance. A page is always 5 credits, so the cost of a run is pages × 5.
  • An early stop keeps what you have. A bad key, an empty balance (402) or an unknown account (404) ends the run but still writes the CSV. Timeouts (408), concurrency limits (429), server errors and dropped connections are retried with a growing pause; if they keep failing, the pages collected so far are saved too. Failed calls are refunded.
  • Pages run one after another. Each needs the previous page's cursor, so one list can't be fetched in parallel. To export many accounts faster, run one list per worker, up to your plan's concurrency limit.

Run it

A 422-follower coffee account, paged to the end:

python ig_follows.py guakita_caffe
@guakita_caffe: 420 of 422 followers in 9 pages (45 credits), saved to guakita_caffe_followers_20261027-153700.csv
  123 private, 7 verified, 61 with a story up

A creator whose follower list Instagram cuts short:

python ig_follows.py @lancehedrick
@lancehedrick: 50 of 134,706 followers in 1 page (5 credits), saved to lancehedrick_followers_20261027-153434.csv
  10 private, 1 verified, 4 with a story up
  Instagram shows only 50 of this account's followers: the list ends there, so no further pages exist.

The same creator's following list is complete, but at 1,285 accounts it needs more than the default 20 pages:

python ig_follows.py lancehedrick --following --pages 30
@lancehedrick: 1,281 of 1,285 following in 26 pages (130 credits), saved to lancehedrick_following_20261027-154255.csv
  76 private, 245 verified, 485 with a story up

The 26 pages took 88 seconds in all, between 2.4 and 5.3 seconds each. With the default --pages 20 the run stops at 996 accounts and says so. The CSV has one row per account:

user_id,username,full_name,is_private,is_verified,has_story
8188331,darylrbueno,Daryl Bueno,False,True,True
…

You don't need the whole list to answer that. Add query and Instagram searches the list by username and name in one call. The catch: on lists Instagram limits, the search is silently ignored and you get unrelated accounts back. Those pages say so with should_limit_list_of_followers (or should_limit_list_of_followings) set to true, so check it before you read an empty result as "no":

from ig_follows import call


def in_list(account, name, kind="followers"):
    """True or False, or None when Instagram doesn't let you search this list."""
    data = call(f"/{kind}/by_user_id", {"user_id": account, "query": name})
    if data.get("should_limit_list_of_" + ("followers" if kind == "followers" else "followings")) is not False:
        return None  # limited (the search is ignored) or private (no flag, no list)
    return any(u["username"].lower() == name.lower() for u in data.get("users", []))


print(in_list("specialtycoffeeassociation", "crownandscale"))  # True
print(in_list("lancehedrick", "jimseven", kind="following"))  # True
print(in_list("lancehedrick", "jimseven"))                     # None
print(in_list("cafecoadoclube", "zzqqxxnotarealname"))         # False
print(in_list("61111972795", "jimseven"))                      # None (a private account)

Those are the real answers. A private account's page carries no flag at all (and no list), so the check trusts only an explicit false. The flag is a better guide than size: Lance Hedrick's follower list carried it, and so did the 422-follower list from the first run, which pages to the end but ignores the search too (it returned 25 unrelated accounts for each of the 15 names we tried). Each search is one call, 5 credits.

Accounts two creators both follow

Following lists are where niche research gets interesting: the brands, shops and people that several creators in a space follow are usually the ones that matter in it. Export two lists, then intersect them on the account ID. Save this as both.py:

import csv
import sys


def load(path):
    with open(path, newline="", encoding="utf-8") as f:
        return {r["user_id"]: r for r in csv.DictReader(f)}


a, b = load(sys.argv[1]), load(sys.argv[2])
both = sorted((r for i, r in a.items() if i in b),
              key=lambda r: (r["is_verified"] != "True", r["username"]))
print(f"{len(both)} accounts are in both lists ({len(a):,} and {len(b):,})")
for r in both[:int(sys.argv[3]) if len(sys.argv) > 3 else 20]:
    badge = "verified" if r["is_verified"] == "True" else ""
    print(f"  @{r['username']:<28} {r['full_name'][:30]:<30} {badge}")
python ig_follows.py jimseven --following --pages 30
python both.py lancehedrick_following_20261027-154255.csv jimseven_following_20261027-154348.csv 8
128 accounts are in both lists (1,281 and 1,122)
  @acaiacoffee                  Acaia                          verified
  @agarojewska                  Agnieszka Rojewska             verified
  @baristahustle                Barista Hustle                 verified
  @baristamagazine              Barista Magazine               verified
  @coffeeandlucas               Federico Lucas Pezzetta 📸      verified
  @coffeecollectif              Coffee Collective              verified
  @cometeer                     Cometeer Coffee                verified
  @coolroastcurves              Cool Roast Curves              verified

James Hoffmann's following list took 23 pages (115 credits), so the comparison cost 245 credits for both lists, about 4.5 cents on Starter. 36 of the 128 shared accounts are verified; the rest include grinder makers, roasters and a coffee festival.

New and lost followers between two exports

Exporting the same account on a schedule and comparing the files looks like an easy way to see who followed and who left. Save this as compare.py:

import csv
import sys


def load(path):
    with open(path, newline="", encoding="utf-8") as f:
        return {r["user_id"]: r for r in csv.DictReader(f)}


old, new = load(sys.argv[1]), load(sys.argv[2])
gained = [r["username"] for i, r in new.items() if i not in old]
missing = [r["username"] for i, r in old.items() if i not in new]
print(f"{len(old)} -> {len(new)} accounts: {len(gained)} new, {len(missing)} missing")

We exported the 422-follower account twice, ten minutes apart. Each run writes its own timestamped file, so both are there to compare:

python compare.py guakita_caffe_followers_20261027-153700.csv guakita_caffe_followers_20261027-154637.csv
420 -> 406 accounts: 1 new, 15 missing

user_count said 422 both times. The second export's last page simply held 6 accounts instead of 21, so at least 14 of the 15 "missing" followers hadn't gone anywhere. Treat a follower as lost only when they are missing from two exports in a row, compare exports whose row count is close to user_count, and on lists without the limit flag confirm with in_list() before you act on it.

Cost and speed

  • 5 credits per page of up to 50 accounts. 1,000 accounts is 20 pages, 100 credits, about 1.8 cents on Starter ($49.99 for 275,000 credits). 10,000 followers is about 1,000 credits, roughly 18 cents.
  • About 1,000 accounts a minute per list. Pages took 1.3 to 6.2 seconds in our runs. The Specialty Coffee Association's 352,974 followers would be about 7,060 pages, 35,300 credits and several hours; we checked only its first two pages.
  • Failed calls cost nothing, and a missing account is a free 404. A private account's empty list is a successful call, 5 credits.

Using follower data responsibly

Follower lists are lists of people. Keep only the columns you need, don't use them to message people in bulk, and if you work with data about people in the EU or UK, the GDPR applies to what you store. The terms of service cover what you may do with data from ScrapingBot.

Where to go from here

Common questions

Is there an official Instagram API for follower lists?

No. Meta's Instagram API gives followers_count and follows_count for business and creator accounts, but none of its endpoints lists the accounts behind those numbers (checked October 2026). To get the list of a public account's followers or following, you need a data API such as ScrapingBot's /followers/by_user_id and /following/by_user_id.

How do I get all followers of an Instagram account?

Call /followers/by_user_id with the account's username or numeric ID, then send each page's next_max_id back as max_id while has_more is true. Each page holds up to 50 accounts and costs 5 credits. In our test, a 422-follower account came back as 420 accounts in 9 pages.

Why do I only get about 50 followers for some accounts?

Instagram cuts some accounts' follower lists short. For Lance Hedrick (134,706 followers), Morgan Eckroth (1.39 million) and NASA (104 million), the first page was the only page: has_more was false. The Specialty Coffee Association, with 352,974 followers, paged normally, so it is not a size threshold. Compare the accounts you got with user_count to spot it.

Can I search an account's followers for one name?

Often. Add query with a username or name and Instagram searches the list. When a page carries should_limit_list_of_followers: true, the search is ignored and you get unrelated accounts back, so check that flag before trusting a "not found". In our checks the search worked on the Specialty Coffee Association's followers and on Lance Hedrick's following, and was ignored on his followers.

Can I get the followers of a private account?

No. For a private account the call succeeds with an empty users list and no user_count, and it costs 5 credits like any page. Only public accounts' lists are available.

How much does it cost to export 10,000 followers?

About 200 pages at 5 credits each: 1,000 credits, roughly $0.18 on the Starter plan ($49.99 for 275,000 credits). The 100 free credits cover 20 pages, up to about 1,000 accounts.

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.