By the end of this tutorial, you will have a short Python script that turns public Instagram usernames and post links into structured rows: caption, like and comment counts, image link and date for each post, plus follower, following and post counts for each profile.
We use two Apify Actors that share one input style: Instagram Posts & Engagement Scraper (reventadata/instagram-scraper) for posts, and Instagram Profile & Followers Scraper (reventadata/instagram-profile-scraper) for profile stats. Both read the public pages a logged-out browser sees and send no login.
The two sample rows below are a sample from a capture dated 2026-10-22 (a National Geographic post) and a sample from a capture dated 2026-10-17 (the @instagram profile), parsed with each Actor's own parser.
⏱️ Time estimate: one sitting: install one package, set one token, run two short Python blocks.
Prerequisites
To follow this tutorial, you will need:
- Python 3.11+ (
apify-client3.3.0 requires it). - The Apify API client for Python.
- An Apify account and its API token, which sits on the API & Integrations page in Apify Console.
Install the client:
pip install "apify-client~=3.0" The code reads the token from an environment variable:
export APIFY_API_TOKEN="your_apify_token_here" What you'll achieve
You will get two datasets from one script:
- Posts: one row per post with
id(the shortcode),url,caption,likesCount,commentsCount,imageUrl,author(the account's display name) andtimestamp. - Profiles: one row per username with
username,fullName,biography,followersCount,followingCount,postsCount,avatarUrlandurl.
Note: Both Actors are unofficial and not affiliated with Instagram or Meta. They do not read comments, follower lists or private accounts.
Example use case: Suppose you track a handful of brand or creator accounts. The profile Actor gives you each account's follower and post counts in one row; the posts Actor gives you the captions and engagement of each account's newest posts. Join them on the username and you have a small engagement table you can refresh on a schedule.
Step 1: Access the two Actors
Go to the Instagram Posts & Engagement Scraper and the Instagram Profile & Followers Scraper pages on Apify Store. Each README lists every input and output field. In this tutorial we drive both Actors from Python, so everything after this step happens in code.
Step 2: Configure the inputs
Posts Actor. It takes two kinds of target, and you can mix them:
-
directUrls: post or reel links such ashttps://www.instagram.com/p/DeuOR-CjKE9/. A link without/p/or/reel/is read as a profile page. -
usernames: public usernames such asnatgeo(a leading@is stripped). For each one the Actor opens the profile page and saves the newest posts it lists. Per the README, that was 12 per profile when checked on 2026-10-22; older posts are not reached.
Profile Actor. It takes usernames only. A handle, an @handle or a full profile URL all work: @natgeo and https://www.instagram.com/natgeo/ both become natgeo.
Both Actors have maxItems (1 to 5,000, default 20), which caps the rows saved in the run. A value outside that range is refused before the run starts.
Here is the setup. It defines one helper that runs an Actor and returns its rows:
import os from apify_client import ApifyClient # 1. Initialize ApifyClient with the token from the environment client = ApifyClient(os.environ["APIFY_API_TOKEN"]) # 2. Run an Actor, wait for it, and return its dataset rows def run_actor(actor_id, run_input): run = client.actor(actor_id).call(run_input=run_input) if run is None or run.status != "SUCCEEDED": status = run.status if run else "unknown" raise SystemExit(f"{actor_id} did not succeed (status: {status}).") return client.dataset(run.default_dataset_id).list_items().items 💡 Tip: start with maxItems: 5, the value both input forms prefill, until your downstream code handles the fields.
Step 3: Add the optional filter and proxy
The profile Actor has one filter: minFollowers. Profiles below it are dropped before they are saved, and so is a profile whose follower count could not be read.
Both Actors take proxyConfiguration. Leave it out: when none is given, they fetch through Apify Unblocker (proxy group UNBLOCKER).
| Parameter | Actor | What it does |
|---|---|---|
directUrls | posts | Post, reel or profile links. |
usernames | both | Profiles to read (posts: newest listed posts; profiles: one row each). |
maxItems | both | Rows saved at most, 1 to 5,000. |
minFollowers | profiles | Drop profiles with fewer followers. |
proxyConfiguration | both | Apify proxy settings; Unblocker when unset. |
Step 4: Run the Actors
Now call the helper once per Actor. Each call() starts the Actor on Apify and waits for it to finish before returning the run:
# 3. Newest posts of one profile, at most 5 rows posts = run_actor( "reventadata/instagram-scraper", {"usernames": ["natgeo"], "maxItems": 5}, ) # 4. Profile stats, one row per username profiles = run_actor( "reventadata/instagram-profile-scraper", {"usernames": ["instagram", "natgeo"], "maxItems": 5}, ) for p in profiles: print(f"@{p['username']}: {p['followersCount']} followers, {p['postsCount']} posts") for post in posts: print(f"{post['timestamp']} | {post['likesCount']} likes | {post['url']}") Behind the scenes:
- For a username, the posts Actor reads the profile page, takes the post codes it lists, then opens each post page and reads its caption, counts and image.
- A post or profile already saved in the run is not saved twice.
- Each Actor stops at
maxItemsor when it runs out of targets.
Step 5: View and download results
Here is one post row, a sample from a capture dated 2026-10-22, parsed with the posts Actor's own parser (image link shortened). A saved row also carries scrapedAt, added at save time:
{ "id": "DeuOR-CjKE9", "url": "https://www.instagram.com/p/DeuOR-CjKE9/", "caption": "Some of the best reasons to travel in 2027 are far from the usual destinations. 🌎\n\nNational Geographic’s picks for Best of the World 2027 are here and features 25 destinations across six continents. \n\nWhether you're drawn to places steeped in history or the thrill of exploring the wild, there's no shortage of experiences worth planning a trip around. \n\nFind your next adventure and see our full #BestOfTheWorld 2027 guide at the link in bio. \n\nPhotograph by Thomas Ekström", "likesCount": 12000, "commentsCount": 147, "imageUrl": "https://scontent.cdninstagram.com/v/t51.82787-15/858921974_18710195065019133_277441153414192961_n.jpg?stp=c216.0.648.648a_dst-jpg_e35_s640x640_tt6&...", "author": "National Geographic", "timestamp": "October 20, 2026" } And one profile row, a sample from a capture dated 2026-10-17, parsed with the profile Actor's own parser (the avatar link is a stand-in from the test capture):
{ "id": "instagram", "username": "instagram", "fullName": "Instagram", "biography": "Discover what's new on Instagram 🔎✨", "followersCount": 687000000, "followingCount": 308, "postsCount": 8620, "avatarUrl": "https://scontent.cdninstagram.com/v/avatar.jpg", "url": "https://www.instagram.com/instagram/" } Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.
Understanding your results
The fields that matter most:
-
likesCount,commentsCount: integers, rounded the way Instagram shows them. The captured post's page said12K likes, 147 comments, so the row holds12000and147. Treat values in the thousands as approximate. -
followersCount,followingCount,postsCount: same rule.687Mfollowers became687000000;8,620posts became8620. -
timestamp: the post date as Instagram writes it (October 20, 2026), not an ISO timestamp. Parse it yourself if you need a date type. -
author: the display name (National Geographic), not the handle. The handle is the username you passed in. -
caption: the full caption, line breaks kept as\n. -
imageUrl,avatarUrl: Instagram CDN links. They expire, so download the image soon if you need it. -
url: posts always come back as/p/<shortcode>/, reels included; profiles ashttps://www.instagram.com/<username>/. -
fullName: falls back to the username when the page shows no name, so a name equal to the handle may mean no name was shown.
Pro tips
- Mix links and usernames in one posts run. Put specific post links in
directUrlsand accounts inusernames; duplicates are saved once. - Use the profile Actor as a pre-filter. Run it with
minFollowerson a long list of candidates, then pass only the usernames that came back to the posts Actor. - Expect the newest posts only. A username yields the posts its profile page lists, not the full history. For an older post, pass its link in
directUrls. - Normalise the date column.
timestampis text such asOctober 20, 2026; convert it withdatetime.strptime(value, "%B %d, %Y")before sorting. - Read rows with
iterate_items()for big runs.list_items()makes one API call;iterate_items()pages through every row. - Give each integration its own token. Apify's API docs advise a token with a clear description per service, never one token shared across several, the same way you would not reuse one password for different accounts.
Troubleshooting
- Run FAILED with zero rows: both Actors fail a run when a page is blocked or cannot be read (a typo in a username, a login wall), and the posts Actor also fails when no input is given (the profiles Actor ends SUCCEEDED with no rows); an over-strict
minFollowerson profiles that loaded ends SUCCEEDED with an empty dataset and a status message saying so. - Fewer posts than
maxItems: a username yields only the posts its profile page lists. A profile page with no post list is skipped with a warning in the log. - Check the log: a summary line near the end reads
Run finished: pushed N records to dataset.From Python:client.log(run.id).get(), withrunfromcall().
For help, use the Actor's Store page.
Pricing and next steps
For current pricing and details, see the Store page.
To refresh the table, save each input as a task and add an Apify schedule, which runs an Actor or task on a cron expression (0 8 * * * is every day at 8 AM). To react when a run finishes, add a webhook on ACTOR.RUN.SUCCEEDED; its payload carries the run object, including defaultDatasetId.
- Instagram Posts & Engagement Scraper on Apify Store
- Instagram Profile & Followers Scraper on Apify Store
- Apify Python client documentation
Which accounts are you planning to track? Drop a comment! 💬
