Install
openclaw skills install skills-sh:jeniok/skills/instagram-scraperInstagram Scraper Public Instagram data via Apify's Instagram Scraper. No Instagram login, no cookies, no Meta app review. | | | |---|---| | Actor | apify/instagram-scraper | | Auth | Authorization: Bearer $APIFYTOKEN, or a logged-in Apify CLI — see Setup | | Price | from…
openclaw skills install skills-sh:jeniok/skills/instagram-scraperPublic Instagram data via Apify's Instagram Scraper. No Instagram login, no cookies, no Meta app review.
| Actor | apify/instagram-scraper |
| 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 |
Always run this preflight first. Do not attempt a data call until it passes.
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 npx fallback costs about four seconds,
so it only runs when an Apify config directory shows a previous login — a first-time
user reaches AUTH_MISSING instantly rather than waiting for a package download that
was never going to find a session.
$HOME and $USERPROFILE are both checked on purpose. On Windows they can point
to different places — sandboxes and some CI images remap $HOME while the Apify CLI
keeps writing to $USERPROFILE\.apify. Testing only $HOME there reports
AUTH_MISSING 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 AUTH_MISSING on a
machine you believe is authenticated, check both paths before trusting it.
The two AUTH_OK modes are not interchangeable — the preflight tells you which
call form to use.
AUTH_OK curl — a token is in the environment. The HTTP calls in this skill work
as written.AUTH_OK cli: <prefix> — the Apify CLI holds the session and the token is not
readable from disk. ~/.apify/auth.json carries account metadata only (username,
plan, proxy groups — no token field); current CLI versions keep the token in the
OS secrets backend. Do not try to extract one from that file: a bogus
Authorization: Bearer header returns 401 and looks exactly like a revoked
token. Use apify call instead, prefixed with whatever the preflight printed
after cli: (apify, or npx --yes apify-cli@latest when the CLI is not on
PATH).AUTH_OK cliEvery payload in this skill still applies — write it to a file and hand it to
apify call instead of curl:
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
apify call runs the Actor
defined by a local .actor/actor.json — inside an Actor repo that silently runs
the wrong thing.--input-file over inline -i '{...}'. Inline JSON has to survive the
shell, and PowerShell and cmd.exe mangle the quoting. A file never does.
--input-file - reads stdin.--output-dataset prints the dataset to stdout — the same array
run-sync-get-dataset-items returns, so every field in the Output section below is
identical. --silent keeps run logs off stdout so the output parses as JSON.apify call waits for the run to finish, so the async polling pattern below is
only needed on the curl path. Add --timeout <seconds> to bound a long job.apify info prints
it — worth showing the user if they may have more than one.AUTH_MISSINGOpen 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):
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:
timeout 300 npx --yes apify-cli@latest login -m console
-m console is required. Bare apify login first prompts for a login method
and will hang forever on stdin the agent cannot answer. -m console goes straight
to the browser flow.timeout 300 bounds it, so an abandoned sign-up doesn't hang the session. Exit
code 124 means it timed out — the user walked away, so ask whether they still want
to continue rather than silently retrying.<username>. Your
token is stored in your OS keyring." From then on the preflight resolves to
AUTH_OK cli 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 |
|---|---|---|
AUTH_OK cli: <prefix> | Logged in and persisted | Continue with the request |
AUTH_MISSING | Login did not complete | Say so plainly and ask whether to retry — do not loop |
The preflight's CLI branch is just apify info, which exits 0 when a session
exists and non-zero when it doesn't. It is the same check that produced
AUTH_MISSING 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 124 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.
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 (apify info'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:
# the user runs this in their own shell / CI secret store — not via the agent
export APIFY_TOKEN="…"
The preflight then reports AUTH_OK curl 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:
npx --yes apify-cli@latest login -m manual
Avoid login -t <token>. 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
$APIFY_TOKEN 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.
One call, synchronous, returns the items directly. Good for anything that finishes
inside ~60 s. This is the AUTH_OK curl form — under AUTH_OK cli, put the
same -d payload in a file and run it through apify call as shown above.
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 User-Agent header is only so runs originating from this skill can be told apart
in logs. It carries no personal data and can be removed.
resultsType| Value | Returns |
|---|---|
details | Profile metadata: followers, following, bio, post count, profile picture. Cheapest — 1 result per profile. |
posts | A feed of posts |
reels | Reels only |
stories | Currently-live stories |
comments | Comments on a post URL |
mentions | Posts mentioning an account |
Pick details whenever the user only needs profile numbers. Using posts for that
question costs up to 100× more for no benefit.
| Field | Type | Default | Notes |
|---|---|---|---|
directUrls | array | — | Profile, post, reel, hashtag, location or audio URLs — see URL handling below |
resultsType | string | posts | See table above |
resultsLimit | integer | 100 | Per URL. Drives cost — always set it. |
onlyPostsNewerThan | string | — | YYYY-MM-DD, ISO, or 1 day / 2 months. UTC |
search | string | — | Keyword, instead of directUrls |
searchType | string | hashtag | hashtag, profile, place, user |
searchLimit | integer | 10 | Max items discovered per search |
addParentData | boolean | false | 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 |
|---|---|---|---|
addProfileStatistics | boolean | details | Adds a statistics object (~60 fields) — account_type (1 Personal, 2 Business, 3 Creator), media_count, total_clips_count, category, city_name, contact fields. Works on private profiles too |
skipPinnedPosts | boolean | posts | Exclude pinned posts |
isNewestComments | boolean | comments | Newest-first ordering. Paid plans only — free plans get default order |
includeNestedComments | boolean | comments | Include replies. Paid plans only. Each reply is a separate result, so totals exceed resultsLimit |
directUrls is more forgiving than it looks. All of these are accepted:
instagram.com/_u/natgeo/profilecard/ — _u and profilecard are strippedinstagram.com/stories/username/ — reduced to the usernameinstagram.com/share/BAC6cDeb_- — resolved to the canonical post URLinstagram.com/explore/locations/7538318/ — the ID alone is valid, no slug neededNot supported: numeric post IDs in URL form (instagram.com/p/3369450800358839406/)
for posts, reels, mentions or details. Use the shortCode form instead. This
format does work for comments.
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 resultsType: "details" yields place details, not profile details.
directUrls and search cannot be combined; if both are
present the URLs win and the search is ignored.travel, never #travel."travel, fitness".Every capability the Actor exposes, with the payload for each. Swap the -d '...'
into the curl above.
Profile stats for several accounts — cheapest possible call:
-d '{"directUrls":["https://www.instagram.com/nasa/","https://www.instagram.com/natgeo/"],"resultsType":"details","resultsLimit":1}'
A profile's recent posts, last 30 days:
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"posts","resultsLimit":30,"onlyPostsNewerThan":"1 month"}'
A profile's reels:
-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:
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"stories","resultsLimit":20}'
Posts that mention an account — brand monitoring:
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"mentions","resultsLimit":50}'
Comments on one post:
-d '{"directUrls":["https://www.instagram.com/p/SHORTCODE/"],"resultsType":"comments","resultsLimit":50}'
Posts under a hashtag:
-d '{"search":"wildlifephotography","searchType":"hashtag","searchLimit":50,"resultsType":"posts","resultsLimit":50}'
Find accounts by keyword — user returns account records:
-d '{"search":"climate photographer","searchType":"user","searchLimit":20,"resultsType":"details"}'
Search profiles by name — profile matches on profile pages:
-d '{"search":"national geographic","searchType":"profile","searchLimit":20,"resultsType":"details"}'
Posts from a place:
-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:
-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, addParentData stamps each item with its source so the results
can be grouped afterwards:
-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:
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"details","resultsLimit":1,"addProfileStatistics":true}'
Recent posts, excluding pinned ones:
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"posts","resultsLimit":30,"skipPinnedPosts":true}'
Only the pinned posts — invert the same pair:
-d '{"directUrls":["https://www.instagram.com/nasa/"],"resultsType":"posts","skipPinnedPosts":false,"onlyPostsNewerThan":"0 minutes"}'
Newest comments first, including replies — both paid-plan only:
-d '{"directUrls":["https://www.instagram.com/p/SHORTCODE/"],"resultsType":"comments","resultsLimit":50,"isNewestComments":true,"includeNestedComments":true}'
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 (resultsType: "posts") — 23 fields:
{
"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 (resultsType: "reels") — 31 fields. Posts plus video:
{
"…all post fields…": "…",
"videoUrl": "https://…", "videoDuration": 28.28,
"videoViewCount": 120433, "videoPlayCount": 98221,
"audioUrl": "https://…", "musicInfo": {}, "productType": "clips",
"isPinned": false
}
Comment (resultsType: "comments"):
{
"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": "…" }
}
repliesCount and replies populate only with includeNestedComments: true (paid).
Profile details (resultsType: "details") — 22 fields. Verified live:
{
"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": []
}
details already includes latestPosts (up to 12) and relatedProfiles (up to 48)
at no extra cost. If the user wants a profile's numbers and a look at recent posts,
one details call covers both — a second posts call is usually waste.
With addProfileStatistics: true a statistics object is appended (~60 fields):
account_type (1 Personal, 2 Business, 3 Creator), media_count, total_clips_count,
category, city_name, address_street, zip, bio_links, mutual_followers_count.
Mentions (resultsType: "mentions") — 21 fields, post-shaped, plus taggedUsers,
music, carouselImages, carouselImageCount.
Place details (location URL) — 16 fields:
{
"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:
name, postsCount, url, id, posts, postsPerDay, difficulty, related,
frequent, average, rare, relatedFrequent, relatedAverage, relatedRare.
Search results carry searchTerm and searchSource so you can tell which query
produced each row:
searchTerm, searchSource, name, postsCount, url, idsearchTerm / searchSourceownerUsername, ownerFullName, taggedUsersEvery 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.
For large jobs, start async and poll rather than holding a sync connection:
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 status is SUCCEEDED, then fetch items. FAILED or ABORTED means stop
and report — do not silently retry a job the user is paying for.
| Status | Meaning | What to do |
|---|---|---|
401 | Token missing, wrong, or revoked | Re-run the Setup preflight above. Do not retry the call. A 401 on the curl path while the CLI is logged in means the token was invented — switch to apify call. |
402 / insufficient credit | 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. |
404 | Bad Actor ID or run ID | Check the URL uses apify~instagram-scraper with a tilde, not a slash. |
408 / 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. |
apify call 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 apify info 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.
likesCount: -1 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.The user is paying per result. Treat that as real money.
resultsLimit. The default is 100 per URL; a five-URL call with the
default costs 500 results when the user probably wanted 25.resultsType: "details" for any question about followers, bio or post
counts. It returns one result per profile.401 or 402 — each attempt can be billable and
none of them will succeed.41a771fa05d5