API tutorial · September 2026

How to Export Instagram Followers List via API (Without Getting Banned)

Which followers data an API can actually give you, which it cannot, and why the tools that promise the full list are the ones that put accounts at risk.

· 12 minute read

Let us remove the ambiguity at the start, because it saves you a wasted week. You cannot download the list of individual followers of another Instagram account through a public-data API — including this one. That is not a limitation we are hiding behind a paywall; it is the honest state of the data. What you can export, reliably and at scale, is follower counts, and the growth of those counts over time. For the majority of real jobs — sizing an audience, tracking growth, comparing candidates — counts are what you actually needed anyway.

This guide splits the topic cleanly: what is exportable, how to export it correctly, why the “full followers list” tools are dangerous, and how to run the whole thing without tripping rate limits.

What “export Instagram followers” actually means

GoalPossible via public-data API?What you use
Follower count for an account (or many accounts)Yes/v1/user/by/usernamefollower_count
Follower count over time (growth tracking)YesThe same call, on a schedule, stored as snapshots
Audience size compared across a candidate listYesBatch the profile call, export to CSV
Engagement context (posts, reels, comments) for those accountsYesPosts, reels, comments endpoints
The list of individual follower usernamesNoNot offered. Any tool claiming it is doing something else — see the risk section.
Follower email addresses or phone numbersNoFiltered from responses by design (see the creator data guide)

Do not build on a follower-list promise. If a pipeline’s core requirement is “give me the usernames of everyone following X”, no public-data API will satisfy it, and a tool that appears to is likely using your own logged-in session — the exact pattern that gets accounts actioned. Redesign the job around counts, engagement, and public content.

Exporting follower counts

One call returns the count. Resolve the profile by username and read follower_count:

curl — one account
curl -s -H "Authorization: Bearer gs_your_key" \
  "https://gramscraper.com/api/instagram/v1/user/by/username?username=natgeo"
Response (trimmed)
{
  "pk": 787132,
  "username": "natgeo",
  "full_name": "National Geographic",
  "follower_count": 291166543,
  "following_count": 266,
  "media_count": 1667,
  "is_verified": true,
  "is_private": false
}

follower_count is an integer. That single field is the whole export for a sizing job — at 1 credit per account.

Batch export to CSV

Real jobs mean hundreds or thousands of handles. The pattern is a bounded worker pool: modest concurrency, a retry that honours Retry-After, and a CSV at the end.

Node.js 18+ — export many accounts to CSV
const BASE = "https://gramscraper.com/api/instagram";
const KEY = process.env.GRAMSCRAPER_API_KEY;
const CONCURRENCY = 5;          // keep this modest
const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function fetchProfile(username, attempt = 0) {
  const url = new URL(`${BASE}/v1/user/by/username`);
  url.searchParams.set("username", username);
  const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });

  if (!res.ok) {
    if (RETRYABLE.has(res.status) && attempt < 4) {
      // Honour Retry-After, else exponential backoff with jitter.
      const hdr = Number(res.headers.get("retry-after"));
      const wait = Number.isFinite(hdr) && hdr > 0
        ? hdr * 1000
        : Math.min(500 * 2 ** attempt, 30_000) + Math.random() * 250;
      await new Promise(r => setTimeout(r, wait));
      return fetchProfile(username, attempt + 1);
    }
    return { username, error: `HTTP ${res.status}` };
  }
  const p = await res.json();
  return {
    username: p.username,
    full_name: p.full_name,
    follower_count: p.follower_count,
    following_count: p.following_count,
    media_count: p.media_count,
    is_verified: p.is_verified,
  };
}

async function exportAll(handles) {
  const rows = [];
  const queue = [...handles];
  const workers = Array.from({ length: CONCURRENCY }, async () => {
    while (queue.length) rows.push(await fetchProfile(queue.shift()));
  });
  await Promise.all(workers);

  const cols = ["username","full_name","follower_count","following_count","media_count","is_verified","error"];
  const csv = [
    cols.join(","),
    ...rows.map(r => cols.map(c => JSON.stringify(r[c] ?? "")).join(",")),
  ].join("\n");

  require("fs").writeFileSync("followers-export.csv", csv);
  return rows;
}

await exportAll(["natgeo", "nike", "instagram"]);
Python 3 — same export with a worker pool
import os, csv, time, random, requests
from concurrent.futures import ThreadPoolExecutor

BASE = "https://gramscraper.com/api/instagram"
HEADERS = {"Authorization": f"Bearer {os.environ['GRAMSCRAPER_API_KEY']}"}
RETRYABLE = {429, 500, 502, 503, 504}

def fetch_profile(username, attempt=0):
    r = requests.get(
        f"{BASE}/v1/user/by/username",
        headers=HEADERS,
        params={"username": username},
        timeout=30,
    )
    if r.status_code in RETRYABLE and attempt < 4:
        wait = r.headers.get("retry-after")
        delay = float(wait) if wait and wait.isdigit() else min(0.5 * 2**attempt, 30)
        time.sleep(delay + random.uniform(0, 0.25))
        return fetch_profile(username, attempt + 1)
    r.raise_for_status()
    p = r.json()
    return {
        "username": p.get("username"),
        "full_name": p.get("full_name"),
        "follower_count": p.get("follower_count"),
        "following_count": p.get("following_count"),
        "media_count": p.get("media_count"),
        "is_verified": p.get("is_verified"),
    }

def export_all(handles, concurrency=5):
    with ThreadPoolExecutor(max_workers=concurrency) as pool:
        rows = list(pool.map(fetch_profile, handles))
    with open("followers-export.csv", "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=rows[0].keys())
        w.writeheader()
        w.writerows(rows)
    return rows

if __name__ == "__main__":
    export_all(["natgeo", "nike", "instagram"])

CONCURRENCY = 5 is a starting point, not a target to raise. Five in-flight requests already completes a thousand-account export in a few minutes, and a burst of twenty is how you manufacture a 429 you did not need. The _meta.credits_remaining field in each response tells you when to stop.

Pagination, and where it does and does not apply

Be precise here, because it trips people up: follower counts do not paginate. A count is one number, so there is no cursor to follow. You page only when you are walking a list.

The list endpoints that do paginate are the ones you use for the engagement side of an audience analysis. They share one rule: stop when the cursor is null, not when a page looks short.

ListCursor to pass back
User posts — /v1/user/medias/chunk, /gql/user/mediasend_cursor / profile_grid_items_cursor
User reels — /v2/user/clips, /gql/user/clipspage_id / max_id
Post comments — /v2/media/commentspage_id (from next_page_id)
Hashtag media — /v1/hashtag/medias/top/chunkend_cursor
Generic safe pagination loop
async function walk(fetchPage, maxPages = 20) {
  const out = [];
  let cursor = null, pages = 0;
  do {
    const page = await fetchPage(cursor);
    out.push(...(page.response?.items ?? []));
    cursor = page.next_page_id || null;
    pages += 1;
  } while (cursor && pages < maxPages);
  return out;
}

The maxPages cap matters most on large collections: a single popular post can carry hundreds of comment pages, and every page is a credit. Decide the depth your analysis needs before you start the walk — the same discipline described in the comments guide.

Tracking growth over time

Because the API returns a live count, growth is a matter of re-running the export on a schedule and diffing. Store one row per account per day and the delta is free.

Node.js — daily snapshot and delta
const today = new Date().toISOString().slice(0, 10);
const snap = await fetchProfile("natgeo");
const prev = loadPrevious("natgeo");   // your own storage

const delta = prev ? snap.follower_count - prev.follower_count : 0;
saveSnapshot({ ...snap, date: today, delta });
console.log(`${today}: ${snap.follower_count.toLocaleString()} (${delta >= 0 ? "+" : ""}${delta})`);

Cost is easy to reason about: 1 credit per account per check. Daily monitoring of 200 accounts is 200 credits a day, which is six days out of the 100 free credits, then comfortably inside a $9 Starter pack that lasts until you have spent it — purchased credits do not expire, so a monthly monitoring job never has to run on a subscription clock.

Why the API approach beats browser extensions

The temptation, when you want the actual follower list, is a browser extension or a “followers export” web tool. Understand what those ask of you:

A managed API inverts all four points. You never log in to Instagram, the extraction runs on the provider’s infrastructure under their sessions and rotation, and the interface you integrate against is a documented REST endpoint rather than an internal one that rotates. The maintenance burden — a real, ongoing cost of DIY scraping — is the thing you are trading credits for. If you are weighing that trade, the why DIY scrapers break piece goes into it properly.

Rate-limit guidance and best practices

Honest limitations

Get 100 free API credits

Export ten accounts, confirm the fields, and see the real cost per call before you commit. Starter is $9 for 10,000 credits that do not expire.

Create a free account →