Instagram Scraper
Public Instagram data via Apify's
Instagram Scraper. No Instagram login, no cookies, no Meta app review.
| |
|---|
| Actor | |
| Auth | Authorization: Bearer $APIFY_TOKEN
, or a logged-in Apify CLI — see Setup |
| Price | from $2.30 per 1,000 results, billed by Apify |
| Free tier | $5/month credits ≈ 2,000+ results, no credit card |
Setup — do this before any data call
Always run this preflight first. Do not attempt a data call until it passes.
bash
if [ -n "$APIFY_TOKEN" ]; then
echo "AUTH_OK curl"
elif command -v apify >/dev/null 2>&1 && apify info >/dev/null 2>&1; then
echo "AUTH_OK cli: apify"
elif { [ -f "$HOME/.apify/auth.json" ] || [ -f "$USERPROFILE/.apify/auth.json" ]; } \
&& npx --yes apify-cli@latest info >/dev/null 2>&1; then
echo "AUTH_OK cli: npx --yes apify-cli@latest"
else
echo "AUTH_MISSING"
fi
The checks are ordered cheapest-first. The
fallback costs about four seconds,
so it only runs when an Apify config directory shows a previous login — a first-time
user reaches
instantly rather than waiting for a package download that
was never going to find a session.
and are both checked on purpose. On Windows they can point
to different places — sandboxes and some CI images remap
while the Apify CLI
keeps writing to
. Testing only
there reports
for a user who is perfectly well logged in, and the skill would then
send them to sign up for an account they already have. If you see
on a
machine you believe is authenticated, check both paths before trusting it.
The two modes are not interchangeable — the preflight tells you which
call form to use.
- — a token is in the environment. The HTTP calls in this skill work
as written.
- — the Apify CLI holds the session and the token is not
readable from disk. carries account metadata only (username,
plan, proxy groups — no field); current CLI versions keep the token in the
OS secrets backend. Do not try to extract one from that file: a bogus
header returns and looks exactly like a revoked
token. Use instead, prefixed with whatever the preflight printed
after (, or
npx --yes apify-cli@latest
when the CLI is not on
).
If the preflight prints
Every payload in this skill still applies — write it to a file and hand it to
instead of
:
bash
cat > /tmp/ig-input.json <<'EOF'
{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"details","resultsLimit":1}
EOF
apify call apify/instagram-scraper --input-file /tmp/ig-input.json --output-dataset --silent
- Always pass the Actor id explicitly. With no id, runs the Actor
defined by a local — inside an Actor repo that silently runs
the wrong thing.
- Prefer over inline . Inline JSON has to survive the
shell, and PowerShell and mangle the quoting. A file never does.
reads stdin.
- prints the dataset to stdout — the same array
run-sync-get-dataset-items
returns, so every field in the Output section below is
identical. keeps run logs off stdout so the output parses as JSON.
- waits for the run to finish, so the async polling pattern below is
only needed on the path. Add to bound a long job.
- The run is billed to whichever account the CLI is logged in as. prints
it — worth showing the user if they may have more than one.
If the preflight prints
Open the Apify sign-up page in the user's browser, then ask for the token. Run this
exactly as-is — it picks the right command per platform and degrades to printing the
URL when there is no browser (CI, SSH, containers):
bash
URL="https://console.apify.com/sign-up?fpr=z8j1nz"
# xdg-open is checked before open: on some Linux distros `open` is openvt, not a browser.
if command -v xdg-open >/dev/null 2>&1; then xdg-open "$URL"
elif command -v open >/dev/null 2>&1; then open "$URL"
elif command -v powershell.exe >/dev/null 2>&1; then powershell.exe -NoProfile -Command "Start-Process '$URL'"
elif command -v cmd.exe >/dev/null 2>&1; then cmd.exe /c start "" "$URL"
else echo "Open this URL in a browser: $URL"
fi
Then tell the user what is happening — but do not wait for a reply:
I've opened Apify's sign-up page in your browser. Instagram data comes through
Apify, which is free to start — $5 of credits every month, no credit card, enough
for around 2,000 results.
Sign up there (or log in, if you already have an account). I'm opening the
authorisation step now too — approve it and this machine stays connected, so you
won't have to do any of this again.
The sign-up link is a referral link.
Immediately run the login. Do not ask the user to confirm they've signed up first —
this command opens Apify's authorisation page, which offers sign-up itself, and then
blocks until the user finishes. The command is the wait, so there is nothing to
detect and no round trip to burn:
bash
timeout 300 npx --yes apify-cli@latest login -m console
- is required. Bare first prompts for a login method
and will hang forever on stdin the agent cannot answer. goes straight
to the browser flow.
- bounds it, so an abandoned sign-up doesn't hang the session. Exit
code means it timed out — the user walked away, so ask whether they still want
to continue rather than silently retrying.
- On success it prints "Success: You are logged in to Apify as . Your
token is stored in your OS keyring." From then on the preflight resolves to
in every future session — no token to paste, store, or leak into
a transcript.
Then re-run the preflight. That is how you know whether it worked — not the
message, and not an assumption that the browser step went fine:
| Preflight now says | Meaning | Do |
|---|
| Logged in and persisted | Continue with the request |
| Login did not complete | Say so plainly and ask whether to retry — do not loop |
The preflight's CLI branch is just
, which exits
when a session
exists and non-zero when it doesn't. It is the same check that produced
a moment earlier, so re-running it is a genuine confirmation rather
than a restatement of what you already believed.
Two failure modes worth naming, because both look like success from the outside:
the user closes the authorisation tab without approving (exit
at the timeout),
and the user signs up but never reaches the approval step. In both the sign-up may
well have succeeded while
this machine is still not connected — which is exactly
why the preflight, not the sign-up, is the thing to trust.
Keep the sign-up tab and the login in that order. The sign-up page is where the
referral is recorded; the authorisation step afterwards is just this machine
connecting to whichever account now exists. Opening them the other way round loses
the attribution.
Never ask the user for their token
Do not request, accept, or handle an Apify API token in the conversation. The
browser login above exists precisely so the secret never reaches the agent: the CLI
receives it directly from Apify and writes it to the OS keyring, and this skill only
ever reads the
result of that (
's exit code), never the value.
If the user offers a token unprompted, decline it and point them at one of the two
safe routes below. Anything pasted into a session is a live credential sitting in a
transcript, in scrollback, and in any log that captures the conversation.
Headless environments — CI, SSH, containers, anywhere the OAuth round trip cannot
open a browser. The user sets the credential themselves, out of band, before
starting the agent:
bash
# the user runs this in their own shell / CI secret store — not via the agent
export APIFY_TOKEN="…"
The preflight then reports
and everything works, with the value never
passing through the conversation.
Or the CLI's own prompt, which reads the token from stdin rather than the
conversation:
bash
npx --yes apify-cli@latest login -m manual
Avoid
. Passing a secret as a command-line argument exposes it in
the process list to every other process on the machine, and in shell history. It also
clears the stored session before validating, so a typo or a stale value logs the
user out of a session that was working.
When a token does legitimately exist in the environment, always reference it as
and let the shell expand it — as every example in this skill does.
Never substitute the literal value into a command, a log line, or a message.
Fetching data
One call, synchronous, returns the items directly. Good for anything that finishes
inside ~60 s.
This is the form — under
, put the
same
payload in a file and run it through
as shown above.
bash
curl -s -X POST \
"https://api.apify.com/v2/acts/apify~instagram-scraper/run-sync-get-dataset-items?timeout=120" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-H "User-Agent: instagram-scraper-skill" \
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"details","resultsLimit":1}'
The
header is only so runs originating from this skill can be told apart
in logs. It carries no personal data and can be removed.
Choosing
| Value | Returns |
|---|
| Profile metadata: followers, following, bio, post count, profile picture. Cheapest — 1 result per profile. |
| A feed of posts |
| Reels only |
| Currently-live stories |
| Comments on a post URL |
| Posts mentioning an account |
Pick
whenever the user only needs profile numbers. Using
for that
question costs up to 100× more for no benefit.
Input reference
| Field | Type | Default | Notes |
|---|
| array | — | Profile, post, reel, hashtag, location or audio URLs — see URL handling below |
| string | | See table above |
| integer | | Per URL. Drives cost — always set it. |
| string | — | , ISO, or / . UTC |
| string | — | Keyword, instead of |
| string | | , , , |
| integer | | Max items discovered per search |
| boolean | | Stamps each item with the query that produced it |
Four more fields work but are not in the published input schema. They are
documented in the Actor's README and verified working — use them freely:
| Field | Type | Applies to | Notes |
|---|
| boolean | | Adds a object (~60 fields) — (1 Personal, 2 Business, 3 Creator), , , , , contact fields. Works on private profiles too |
| boolean | | Exclude pinned posts |
| boolean | | Newest-first ordering. Paid plans only — free plans get default order |
| boolean | | Include replies. Paid plans only. Each reply is a separate result, so totals exceed |
URL handling
is more forgiving than it looks. All of these are accepted:
- Profile IDs work anywhere a profile URL does — a bare numeric ID is fine
instagram.com/_u/natgeo/profilecard/
— and are stripped
instagram.com/stories/username/
— reduced to the username
instagram.com/share/BAC6cDeb_-
— resolved to the canonical post URL
instagram.com/explore/locations/7538318/
— the ID alone is valid, no slug needed
Not supported: numeric post IDs in URL form (
instagram.com/p/3369450800358839406/
)
for
,
,
or
. Use the shortCode form instead. This
format
does work for
.
The URL type drives the output schema. Hashtag, location, audio and explore URLs
return their own metadata even when paired with another content mode — so a location
URL with
yields place details, not profile details.
Constraints that will bite you
- One content type per run. There is no way to get posts and comments in a
single call. Run twice.
- URLs beat search. and cannot be combined; if both are
present the URLs win and the search is ignored.
- Hashtags go in as plaintext — , never .
- Multiple search terms are comma-separated in one string: .
- Free plans get about one page of comments per post (~15). Paid plans have no
such cap. Do not report this as an error — say what it is.
Common tasks
Every capability the Actor exposes, with the payload for each. Swap the
into the curl above.
Profile stats for several accounts — cheapest possible call:
bash
-d '{"directUrls":["https://www.instagram.com/nasa/","https://www.instagram.com/natgeo/"],"resultsType":"details","resultsLimit":1}'
A profile's recent posts, last 30 days:
bash
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"posts","resultsLimit":30,"onlyPostsNewerThan":"1 month"}'
A profile's reels:
bash
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"reels","resultsLimit":20}'
A profile's current stories — only returns anything while stories are live, and
often needs a paid plan:
bash
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"stories","resultsLimit":20}'
Posts that mention an account — brand monitoring:
bash
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"mentions","resultsLimit":50}'
Comments on one post:
bash
-d '{"directUrls":["https://www.instagram.com/p/SHORTCODE/"],"resultsType":"comments","resultsLimit":50}'
Posts under a hashtag:
bash
-d '{"search":"wildlifephotography","searchType":"hashtag","searchLimit":50,"resultsType":"posts","resultsLimit":50}'
Find accounts by keyword —
returns account records:
bash
-d '{"search":"climate photographer","searchType":"user","searchLimit":20,"resultsType":"details"}'
Search profiles by name —
matches on profile pages:
bash
-d '{"search":"national geographic","searchType":"profile","searchLimit":20,"resultsType":"details"}'
Posts from a place:
bash
-d '{"search":"Yosemite National Park","searchType":"place","searchLimit":20,"resultsType":"posts","resultsLimit":50}'
Posts from a specific location or hashtag URL — pass the URL directly instead of
searching:
bash
-d '{"directUrls":["https://www.instagram.com/explore/tags/wildlife/"],"resultsType":"posts","resultsLimit":50}'
Tracking which query produced which post — when scraping several hashtags or
profiles in one run,
stamps each item with its source so the results
can be grouped afterwards:
bash
-d '{"search":"wildlife","searchType":"hashtag","searchLimit":30,"resultsType":"posts","resultsLimit":30,"addParentData":true}'
Deep profile statistics — account type, post and reel counts, category, city,
public contact fields. Also works on private profiles:
bash
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"details","resultsLimit":1,"addProfileStatistics":true}'
Recent posts, excluding pinned ones:
bash
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"posts","resultsLimit":30,"skipPinnedPosts":true}'
Only the pinned posts — invert the same pair:
bash
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"posts","skipPinnedPosts":false,"onlyPostsNewerThan":"0 minutes"}'
Newest comments first, including replies — both paid-plan only:
bash
-d '{"directUrls":["https://www.instagram.com/p/SHORTCODE/"],"resultsType":"comments","resultsLimit":50,"isNewestComments":true,"includeNestedComments":true}'
Output
Each content type produces a different schema, and they cannot be combined in one
run. Samples below are abridged to the useful fields; the URL type you pass can
override the shape (a location URL returns place data even under another mode).
Post / carousel (
) — 23 fields:
json
{
"inputUrl": "https://www.instagram.com/p/DZxvMgyH8yR/",
"id": "3923124318436838545",
"type": "Image",
"shortCode": "DZxvMgyH8yR",
"caption": "In the Democratic Republic of the Congo…",
"hashtags": [], "mentions": ["carstenpeter"],
"url": "https://www.instagram.com/p/DZxvMgyH8yR/",
"likesCount": 34512, "commentsCount": 210,
"timestamp": "2026-06-22T17:24:20.000Z",
"displayUrl": "https://…", "images": [], "childPosts": [],
"dimensionsWidth": 1080, "dimensionsHeight": 1350, "alt": "…",
"ownerUsername": "natgeo", "ownerFullName": "National Geographic", "ownerId": "787132",
"firstComment": "…", "latestComments": [], "isCommentsDisabled": false
}
Reel (
) — 31 fields. Posts plus video:
json
{
"…all post fields…": "…",
"videoUrl": "https://…", "videoDuration": 28.28,
"videoViewCount": 120433, "videoPlayCount": 98221,
"audioUrl": "https://…", "musicInfo": {}, "productType": "clips",
"isPinned": false
}
json
{
"postUrl": "https://www.instagram.com/p/DZ5T2XPllXv/",
"commentUrl": "https://www.instagram.com/p/DZ5T2XPllXv/c/18093536360613690",
"id": "18093536360613690",
"text": "We love you NASA 💙🌎🌊",
"ownerUsername": "mavideniz5521__", "ownerProfilePicUrl": "https://…",
"timestamp": "2026-06-22T17:24:20.000Z",
"likesCount": 4, "repliesCount": null, "replies": null,
"owner": { "username": "…" }
}
and
populate only with
includeNestedComments: true
(paid).
Profile details (
) — 22 fields. Verified live:
json
{
"inputUrl": "https://www.instagram.com/nasa/",
"id": "528817151", "username": "nasa", "url": "https://www.instagram.com/nasa/",
"fullName": "NASA", "biography": "Making the seemingly impossible, possible. ✨",
"externalUrl": "https://www.nasa.gov/", "externalUrls": [],
"followersCount": 104423132, "followsCount": 92, "postsCount": 4888,
"verified": true, "private": false,
"isBusinessAccount": true, "businessCategoryName": "Government Agencies",
"joinedRecently": false, "fbid": "17841401474538262",
"profilePicUrl": "https://…", "profilePicUrlHD": "https://…",
"highlightReelCount": 5, "igtvVideoCount": 171,
"latestPosts": [], "relatedProfiles": []
}
already includes (up to 12) and (up to 48)
at no extra cost. If the user wants a profile's numbers
and a look at recent posts,
one
call covers both — a second
call is usually waste.
With
addProfileStatistics: true
a
object is appended (~60 fields):
(1 Personal, 2 Business, 3 Creator),
,
,
,
,
,
,
,
.
Mentions (
) — 21 fields, post-shaped, plus
,
,
,
.
Place details (location URL) — 16 fields:
json
{
"inputUrl": "https://www.instagram.com/explore/locations/7538318/",
"name": "Copenhagen, Denmark", "location_id": "7538318", "slug": "copenhagen",
"lat": 55.6761, "lng": 12.5683,
"location_address": "…", "location_city": "…", "location_zip": "…",
"phone": "…", "category": "…", "price_range": "…",
"media_count": 1284322, "ig_business": "…", "posts": [], "hours": {}
}
Hashtag details (hashtag URL) — 15 fields, including SEO-style extras:
,
,
,
,
,
,
,
,
,
,
,
,
,
.
Search results carry
and
so you can tell which query
produced each row:
- Hashtag search — 6 fields: , , , , ,
- Place search — 17 fields: place-details shape plus /
- Profile search — 13 fields: post-shaped, with , ,
Images are URLs, not files
Every image and video field is a link to Instagram's CDN. Nothing is downloaded, and
those links are signed and expire after a few hours. If the user needs the media
itself, fetch it promptly on their own bandwidth.
Runs longer than a minute
For large jobs, start async and poll rather than holding a sync connection:
bash
RUN=$(curl -s -X POST "https://api.apify.com/v2/acts/apify~instagram-scraper/runs" \
-H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"posts","resultsLimit":1000}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['id'])")
curl -s "https://api.apify.com/v2/actor-runs/$RUN?waitForFinish=60" \
-H "Authorization: Bearer $APIFY_TOKEN" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['status'])"
curl -s "https://api.apify.com/v2/actor-runs/$RUN/dataset/items?format=json" \
-H "Authorization: Bearer $APIFY_TOKEN"
Poll until
is
, then fetch items.
or
means stop
and report — do not silently retry a job the user is paying for.
Errors
| Status | Meaning | What to do |
|---|
| Token missing, wrong, or revoked | Re-run the Setup preflight above. Do not retry the call. A on the curl path while the CLI is logged in means the token was invented — switch to . |
| / | Monthly credits exhausted | Tell the user; they can wait for the monthly reset or upgrade at https://apify.com/pricing?fpr=z8j1nz (referral link). Do not retry. |
| Bad Actor ID or run ID | Check the URL uses with a tilde, not a slash. |
| / timeout | Sync call exceeded the limit | Switch to the async pattern above. |
| Empty array | Private, deleted, or genuinely empty | Report honestly. Do not assume it is a billing problem. |
| exits non-zero | The run failed, or the CLI session is gone | The CLI prints the reason and a run URL — read it rather than retrying. If also fails, the session expired: re-run Setup. |
An empty result is a real answer. Private accounts, deleted posts and quiet hashtags
all legitimately return nothing.
Data quirks to report accurately, not treat as bugs
- means the creator hid the like count. Instagram does not expose
it. Say "hidden by the creator" — never report it as zero or as an error.
- Private profiles generally return nothing. One exception: if a private account
is tagged as a collaborator on a post and any co-author is public, Instagram
treats that post as public and it will appear in results.
- Metrics can differ from what the app shows. Instagram serves slightly different
counts to logged-out visitors, and large counts move constantly. Small discrepancies
are expected.
- Result counts are not guaranteed. There is no fixed cap; you get what Instagram
exposes publicly. To sanity-check what should be available, open the URL in an
incognito window.
Cost discipline
The user is paying per result. Treat that as real money.
- Always set . The default is 100 per URL; a five-URL call with the
default costs 500 results when the user probably wanted 25.
- Use for any question about followers, bio or post
counts. It returns one result per profile.
- Start small. For anything open-ended, run a bounded first pass, show the user
what came back, and confirm before scaling up.
- Say what a large run will cost before starting it. At $2.30/1,000, a 5,000-result
job is about $11.50.
- Never loop the same call after a or — each attempt can be billable and
none of them will succeed.