# Export Instagram Followers to CSV with Python

> Export followers of a public Instagram account to CSV with Python: full script, cursor pagination, resume after a stop, and 429/503 handling.

- URL: https://storiesfly.com/blog/export-instagram-followers-to-csv-python
- Published: 2026-10-21
- Topic: Tracking
- Publisher: StoriesFly (https://storiesfly.com)

## How do you export Instagram followers to a CSV file with Python?

Call the `/v1/user/followers` endpoint in a loop, pass the `next_cursor` from each page as `cursor` in the next call, and write every user to a CSV with `csv.DictWriter`. The script below does this with one dependency (`requests`), saves its place after each page so it can resume, and waits and retries when the API answers 429 or 503. It works for public accounts only.

StoriesFly is our API, so we use it here. It needs no Instagram account, no login and no cookies, only an API key.

## What you need

- Python 3.10 or newer (the script uses `str | None`).
- The `requests` library: `pip install requests`.
- An API key. Sign in with Telegram at [/developers/dashboard](/developers/dashboard) and create a key. It looks like `sf_live_...`. A new account gets 100 free requests, once per Telegram account, which is enough to try everything in this article.

Put the key in an environment variable so it does not end up in your code:

```bash
export STORIESFLY_KEY=sf_live_your_key_here
```

On Windows PowerShell use `$env:STORIESFLY_KEY = "sf_live_your_key_here"`.

The API reference for this endpoint is at [/developers/docs/user-followers](/developers/docs/user-followers).

## The full script

Save this as `export_followers.py`:

```python
"""Export the followers of an Instagram account to a CSV file.

Usage:  STORIESFLY_KEY=sf_live_... python export_followers.py nasa --max-pages 5

Each page is one request. The script saves the cursor after every page, so if
it stops (network, empty balance) the next run continues where it left off.
"""
import argparse
import csv
import json
import os
import time
from pathlib import Path

import requests

API = "https://storiesfly.com/api/v1"
KEY = os.environ["STORIESFLY_KEY"]
FIELDS = ["id", "username", "full_name", "verified", "is_private", "profile_pic_url"]


def fetch_page(username: str, cursor: str | None) -> dict:
    params = {"username": username}
    if cursor:
        params["cursor"] = cursor
    while True:
        r = requests.get(
            f"{API}/user/followers",
            params=params,
            headers={"X-Api-Key": KEY},
            timeout=60,
        )
        if r.status_code == 429:  # too fast: wait as told, costs nothing
            time.sleep(int(r.headers.get("Retry-After", "1")))
            continue
        if r.status_code == 503:  # failed on their side, refunded: try again
            time.sleep(5)
            continue
        if r.status_code != 200:
            err = r.json()
            raise SystemExit(f"{r.status_code} {err['code']}: {err['error']}")
        return r.json()


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("username")
    ap.add_argument("--max-pages", type=int, default=0, help="0 = no limit")
    args = ap.parse_args()
    username = args.username.lstrip("@")

    out = Path(f"{username}_followers.csv")
    state = Path(f"{username}_followers.state.json")

    cursor = None
    if state.exists():
        cursor = json.loads(state.read_text())["cursor"]
        print("Resuming from the saved cursor")

    new_file = not out.exists()
    pages = rows = 0
    with open(out, "a", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS, extrasaction="ignore")
        if new_file:
            writer.writeheader()
        while True:
            page = fetch_page(username, cursor)
            writer.writerows(page["users"])
            f.flush()
            pages += 1
            rows += len(page["users"])
            print(f"page {pages}: +{len(page['users'])} (total {rows})")

            if not page["has_more"]:
                state.unlink(missing_ok=True)
                print("Done: the list has ended")
                break
            cursor = page["next_cursor"]
            state.write_text(json.dumps({"cursor": cursor}))
            if args.max_pages and pages >= args.max_pages:
                print("Stopped at --max-pages; run again to continue")
                break

    print(f"{rows} rows written to {out}, {pages} requests spent")


if __name__ == "__main__":
    main()
```

## Run it: start with a few pages

Large accounts have a lot of followers, and every page costs one request. Do a cheap first run with `--max-pages`:

```bash
python export_followers.py nasa --max-pages 3
```

The output looks like this (sample values):

```text
page 1: +50 (total 50)
page 2: +50 (total 100)
page 3: +50 (total 150)
Stopped at --max-pages; run again to continue
150 rows written to nasa_followers.csv, 3 requests spent
```

Open `nasa_followers.csv` and check the columns. When it looks right, run the same command without `--max-pages` to export the whole list. Because the script remembers the cursor, the full run continues after the pages you already have.

### How much does it cost?

Each page is one request, and the page size follows what Instagram returns, so it varies. The reliable way to estimate: run 3 to 5 pages, divide the rows by the pages, and divide the account's follower count by that number. That is roughly the number of requests the full export will take. Multiply that by the price per request on [/developers/pricing](/developers/pricing).

## The CSV columns

| Column | Meaning |
|---|---|
| `id` | Numeric Instagram user ID. It does not change when the username changes |
| `username` | Current username |
| `full_name` | Display name |
| `verified` | Whether the account has the verified badge |
| `is_private` | Whether the follower's own account is private |
| `profile_pic_url` | Profile picture link |

The `profile_pic_url` values are Instagram CDN links and expire after a while. If you need the pictures, download them right away. For comparing lists over time, use `id`, not `username`.

## How the pagination works

The followers endpoint returns a page of users plus two fields: `has_more` and `next_cursor`. To get the next page you send the same request with `cursor=<next_cursor>`. The loop in `main()` does exactly that and stops when `has_more` is false.

After each page the script writes the new cursor to `nasa_followers.state.json`. If the run stops for any reason (your laptop sleeps, the network drops, the balance runs out), start the same command again. It reads the cursor from the state file and continues. The CSV is opened in append mode and the header is written only once, so there are no duplicates. When the list ends, the state file is deleted.

If you want a fresh export of the same account, delete both `nasa_followers.csv` and `nasa_followers.state.json` first, or rename the old CSV (see the comparison section below).

## Rate limits and errors

The script handles two cases on its own:

- **429 `rate_limited`.** You sent requests too fast. The limit is 1 request per second before the first top-up and 3 per second at the Start level. The script sleeps for the number of seconds in the `Retry-After` header and repeats the call. A 429 is not charged.
- **503 `upstream_unavailable`.** The call failed on our side. The request is refunded, so the script waits 5 seconds and tries again.

Anything else stops the script with the status code, the error code and the message. Errors are JSON like `{"error": "...", "code": "..."}`. The ones you may meet:

| Status | Code | Charged? |
|---|---|---|
| 400 | `bad_request` | No |
| 401 | `unauthorized` | No |
| 402 | `credits_exhausted` | No |
| 403 | `private_account` | Yes |
| 404 | `not_found` | Yes |
| 500 | `internal_error` | Refunded |

Every answer carries headers such as `X-Units-Charged` and `X-Balance-Remaining`, so you can print them if you want to watch your balance during a long export.

## Private accounts

You cannot export followers of a private account. The API answers 403 `private_account` and returns no data. This is by design, not a bug in the script.

## Following list too

To export who the account follows, use the same script with a different endpoint. Change this line in `fetch_page`:

```python
f"{API}/user/followers",
```

to:

```python
f"{API}/user/following",
```

You will also want to change the output file names so the two exports do not mix: replace `_followers` with `_following` in the two `Path(...)` lines in `main()`. The columns are the same. See [/developers/docs/user-following](/developers/docs/user-following).

## Open it in Excel or Google Sheets

The file is UTF-8 CSV.

- **Google Sheets:** File, Import, Upload, choose the file. Sheets reads UTF-8 correctly.
- **Excel:** use Data, From Text/CSV, and make sure the file origin is set to UTF-8. If you double-click the file, Excel may read names with accents or non-Latin letters incorrectly. The import dialog avoids that.

Long numeric IDs can show in scientific notation in Excel. Set the `id` column to text during import to keep all digits.

## Compare two exports to see who unfollowed

Run the export now, rename the file, and run it again a week later. For example, rename the first one to `nasa_followers_monday.csv`, then run the script again (it creates a new `nasa_followers.csv`). Then compare the `id` columns with sets:

```python
import csv

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

old, new = ids("nasa_followers_monday.csv"), ids("nasa_followers.csv")
print("Unfollowed:", len(old - new))
print("New followers:", len(new - old))
```

`old - new` are IDs that were in the first file and are not in the second, `new - old` the other way around. Look them up in the CSV by `id` to get the usernames. Note that an account that was deleted or banned also disappears from the list.

## No-code option

If you do not write code, StoriesFly has a follower export for regular users at [/instagram-followers-export](/instagram-followers-export). The API in this article is for when you want to run it yourself, schedule it, or build it into your own tool.

## A note on use

This works with public data. Follow Instagram's terms and the law in your country, and do not use exported lists for spam or harassment.

## FAQ

### Can I export followers of a private Instagram account?

No. For a private account the API returns 403 `private_account` and no list. Only public accounts can be exported.

### How many requests does a full follower export take?

One request per page. Page size varies, so run 3 to 5 pages with `--max-pages`, divide rows by pages, and divide the follower count by that to estimate the total.

### What happens if the script stops halfway?

Run the same command again. The cursor is saved to a state file after every page, and the script continues from there and appends to the same CSV.

### Do I need an Instagram account or cookies?

No. The script only sends your StoriesFly API key in the `X-Api-Key` header. There is no Instagram login in the code.

### Are failed requests charged?

Rate limit (429), bad request (400), wrong key (401) and empty balance (402) responses are free. A 503 or 500 is refunded. Data, `not_found` and `private_account` answers are charged.

### Can I get the list of accounts someone follows too?

Yes. Change `/user/followers` to `/user/following` in the script, and change the file names. The columns are the same.
