What you need
- A ToolzerHub API key from app.toolzerhub.com/signup, exported as
TOOLZERHUB_API_KEY - Python 3 and
pip install requests - A public Instagram username
- Credits: the free plan includes 50 credits a month, enough for 25 pages
Prefer no code? The Instagram Followers Scraper Actor on Apify does the same job from a form.
Step 1: Fetch one page
The endpoint is GET /v2/instagram/user/followers. Auth is the x-api-key header. username_or_id is required.
curl "https://api.toolzerhub.com/v2/instagram/user/followers?username_or_id=lisbonbakerywpg" \
-H "x-api-key: $TOOLZERHUB_API_KEY"
Static API example (run on 2026-10-07, trimmed to two business accounts and a shortened cursor):
{
"data": {
"users": [
{
"pk": "32365844271",
"id": "32365844271",
"username": "manitobaentryleveldrop",
"full_name": "Manitoba Entry Level Jobs",
"is_private": false,
"is_verified": false
},
{
"pk": "27928287175",
"id": "27928287175",
"username": "landmarkcustomhomes_",
"full_name": "Landmark Custom Homes",
"is_private": false,
"is_verified": false
}
],
"page_size": 24,
"has_more": true,
"page_info": {
"has_next_page": true,
"end_cursor": "QVFBSllTQTZMaS1TNnVibnB5..."
}
}
}
The real response has more fields per user (profile picture URL, badges) and some paging metadata. This page had 24 users.
Step 2: Follow the cursor
Pass page_info.end_cursor from the response as the end_cursor query parameter on the next request. Stop when end_cursor is null or has_next_page is false.
cursor = None
while True:
params = {"username_or_id": "lisbonbakerywpg"}
if cursor:
params["end_cursor"] = cursor
data = requests.get(URL, params=params, headers=HEADERS, timeout=60).json()["data"]
info = data.get("page_info") or {}
cursor = info.get("end_cursor")
if not cursor or not info.get("has_next_page"):
break
Full script
Usage: python export_followers.py <username> [max_pages] [resume_cursor] (0 means no page limit). Tested on 2026-10-07 with Python 3 and requests.
import csv
import os
import sys
import time
import requests
API_KEY = os.environ["TOOLZERHUB_API_KEY"]
URL = "https://api.toolzerhub.com/v2/instagram/user/followers"
FIELDS = ["username", "full_name", "is_private", "is_verified", "id"]
MAX_ATTEMPTS = 5
MAX_WAIT = 60
def get_page(username, cursor=None):
params = {"username_or_id": username}
if cursor:
params["end_cursor"] = cursor
for attempt in range(MAX_ATTEMPTS):
r = requests.get(URL, params=params, headers={"x-api-key": API_KEY}, timeout=60)
if r.status_code == 200:
return r.json()["data"]
if r.status_code not in (429, 503):
r.raise_for_status()
try:
details = r.json().get("error", {}).get("details") or {}
except ValueError:
details = {}
wait = details.get("retryAfter") or r.headers.get("Retry-After") or 5 * (attempt + 1)
wait = min(int(wait), MAX_WAIT)
print(f"{r.status_code}, retrying in {wait}s", file=sys.stderr)
time.sleep(wait)
raise RuntimeError(f"gave up after {MAX_ATTEMPTS} attempts")
def export(username, max_pages=None, cursor=None):
pages, total = 0, 0
# Rows are written page by page, so an error later keeps what was fetched.
# Pass a cursor to resume: rows are appended to the same file.
with open(f"{username}_followers.csv", "a" if cursor else "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
if not cursor:
writer.writeheader()
try:
while True:
data = get_page(username, cursor)
writer.writerows({k: u.get(k) for k in FIELDS} for u in data["users"])
f.flush()
total += len(data["users"])
pages += 1
info = data.get("page_info") or {}
cursor = info.get("end_cursor")
if not cursor or not info.get("has_next_page") or (max_pages and pages >= max_pages):
break
time.sleep(1)
except Exception as e:
print(f"stopped on page {pages + 1}: {e}", file=sys.stderr)
print(f"resume with: python export_followers.py {username} 0 {cursor}", file=sys.stderr)
print(f"{total} followers, {pages} pages, {pages * 2} credits")
if __name__ == "__main__":
args = sys.argv[1:]
export(args[0], int(args[1]) if len(args) > 1 and args[1] != "0" else None, args[2] if len(args) > 2 else None)
python export_followers.py lisbonbakerywpg 3
62 followers, 3 pages, 6 credits
The three pages returned 24, 19 and 19 users. Example rows from lisbonbakerywpg_followers.csv (order changed):
username,full_name,is_private,is_verified,id
manitobaentryleveldrop,Manitoba Entry Level Jobs,False,False,32365844271
landmarkcustomhomes_,Landmark Custom Homes,False,False,27928287175
user_1,Private person,True,False,
user_2,Private person,False,True,
user_3,Private person,True,False,
Rows 3 to 5 are individuals; their usernames, names and ids are replaced (names shortened). Your file has the real values.
Limits
- Page size: 19 to 25 users per page in our runs (pages of 24, 19, 19, 24, 25, 25 on one account). Do not assume a fixed size.
- Credits: 2 credits per request, charged only on success. Errors cost nothing. The
countfield in the response schema was absent in every run, so you cannot read the total up front. - Private accounts: the request succeeds with
"users": [],has_next_page: falseand anullcursor. You get an empty CSV. - Unknown username:
NOT_FOUND, HTTP 404,retryable: false. - Rate limits: plan-dependent and not published.
RATE_LIMITED(429) is normally retryable. Instagram pages that fail upstream come back as 503 witherror.details.retryAfterin seconds. The script waits that long (capped at 60 seconds), falls back to theRetry-Afterheader, then to 5, 10, 15 seconds, and stops after 5 attempts. A 429 carriesRetry-Afteras a header, not indetails. We did not hit a 429 or 503 in the test run, so that branch was not exercised live. - Other errors:
INVALID_REQUEST,UNAUTHORIZEDandPAYMENT_REQUIRED(out of credits) are not retried. The script stops, keeps the rows already written and prints the command to resume from that page. The error envelope is{ "error": { "code", "status", "message", "retryable", "details" } }. - Data: public profiles only. Keep the key server-side.
Sources
- List user followers, https://docs.toolzerhub.com/reference/instagram-v2/user/get-user-followers-v2 (2 credits per request,
end_cursor) - Authentication, https://docs.toolzerhub.com/docs/authentication
- Responses and errors, https://docs.toolzerhub.com/docs/responses-and-errors
- Rate limits, https://docs.toolzerhub.com/docs/rate-limits
- Instagram Followers Scraper Actor, https://apify.com/thenetaji/instagram-followers-scraper