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
| Goal | Possible via public-data API? | What you use |
|---|---|---|
| Follower count for an account (or many accounts) | Yes | /v1/user/by/username → follower_count |
| Follower count over time (growth tracking) | Yes | The same call, on a schedule, stored as snapshots |
| Audience size compared across a candidate list | Yes | Batch the profile call, export to CSV |
| Engagement context (posts, reels, comments) for those accounts | Yes | Posts, reels, comments endpoints |
| The list of individual follower usernames | No | Not offered. Any tool claiming it is doing something else — see the risk section. |
| Follower email addresses or phone numbers | No | Filtered 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 -s -H "Authorization: Bearer gs_your_key" \
"https://gramscraper.com/api/instagram/v1/user/by/username?username=natgeo"{
"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.
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"]);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.
| List | Cursor to pass back |
|---|---|
User posts — /v1/user/medias/chunk, /gql/user/medias | end_cursor / profile_grid_items_cursor |
User reels — /v2/user/clips, /gql/user/clips | page_id / max_id |
Post comments — /v2/media/comments | page_id (from next_page_id) |
Hashtag media — /v1/hashtag/medias/top/chunk | end_cursor |
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.
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:
- They operate on your session. Most need you to log in, then script your authenticated account to walk Instagram’s private endpoints. That activity is attributed to your account, not to a vendor.
- They trigger anti-automation controls. The signals you have probably already read about —
429,challenge_required,checkpoint_required— are aimed exactly at that pattern. See the blocked and challenge guide. - They break without warning. Extension code targeting endpoints and query hashes is fragile; Instagram rotates them, and the reason is documented in the query hash guide.
- They carry a credential risk. Handing an authenticated session to a third-party extension is handing over access to the account. No export is worth that.
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
- Cap concurrency. Five to eight parallel requests is plenty for an export job. More parallelism buys minutes and costs reliability.
- Back off correctly. Retry only transient statuses, honour
Retry-Afterwhen present, add jitter, and cap attempts. Unbounded retry loops turn a soft limit into a hard one — the mechanics are in the rate limits guide. - Watch your own balance. A
402withInsufficient creditsis a billing outcome, not a rate limit. Check_meta.credits_remainingrather than inferring from errors. - Cache identifiers. A username-to-ID lookup costs a credit; store the IDs you resolve so you never pay twice for the same account.
- Only re-fetch what moved. For monitoring, skip accounts whose values have not changed since the last run.
- Respect the platform and the law. Public data only, a lawful basis for processing, and honour opt-outs. Bulk exporting personal data carries obligations that outlast the script.
Honest limitations
- No individual follower list. The API returns
follower_count, not the followers. There is no endpoint, plan, or flag that changes this. - No follower emails or phones. Structured contact fields are filtered from responses by design.
- Counts are a snapshot. You get the number at call time; the trend is something you build by storing results.
- Private accounts return nothing. Check
is_privatefirst. - Rate limits are real. The docs state requests are rate-limited per API key, and a
429means back off, not retry immediately. - Counts can be rounded in the app but exact here. Expect small differences between what a profile screen displays and the API value; trust the API for arithmetic.
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 →