Read public Instagram data over the same async API as the rest of ScrapeGuys: the posts of a public profile, one page at a time, and one post's full record with a page of its comments. Everything is read the way a logged-out visitor sees it, so no Instagram account or credentials are involved on your side.
/api/v1/social-scraper/instagram/profile-posts/— submit: one page of a profile's posts/api/v1/social-scraper/instagram/profile-posts/{job_id}/— poll/api/v1/social-scraper/instagram/post/— submit: one post, with a page of comments/api/v1/social-scraper/instagram/post/{job_id}/— pollBase URL: https://api.scrapeguys.com · Auth: X-API-Key: gsk_… with the instagram:profile_posts and/or instagram:post scope · See the Overview.
instagram:profile_posts and instagram:post are separate scopes; neither implies the other, and no hotel scope implies either. Contact us to have them added to your key. A call outside your key's scopes returns 403 with {"detail": "This API key does not have the 'instagram:profile_posts' scope."}.next_cursor / next_comments_cursor), null on the last page. Send that cursor back in a new job to get the next page. There is nothing to size: one job, one page, one unit of quota. See Cursors and truncation.truncated_reason) so you can retry the same cursor in a later job. See Access limits.Profile posts
One page of a public profile's posts, newest first: 12 posts and the cursor of the next page.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
username | string | Required | — | The profile. @name, name or https://www.instagram.com/name/ are all accepted and normalised to lower-case name. |
cursor | string | Optional | null | The next_cursor of the previous page's answer. Omit or send null for the first page (the newest posts). Max 2,000 chars. |
A pages field is refused with 400: the API pages by cursor, one page per job.
Errors — 400 (a malformed username, a pages count, an over-long cursor, or the Instagram scraper not currently enabled), 401 (missing or invalid key), 403 (key lacks the scope), 429 (rate limit).
Poll for results
GET /api/v1/social-scraper/instagram/profile-posts/{job_id}/ — only the client that submitted a job can read it; any other key gets 404. Polling is free.
status | Meaning |
|---|---|
pending / processing | Not finished — poll again. |
completed | An answer. complete says whether the page you asked for was served. |
failed | No answer. error_message starts with an error code; result.error carries it alone. |
shortcode, id, url, type (image, video, carousel), caption, display_url, accessibility_caption, product_type (feed, clips for a reel, carousel_container), owner and taken_at, which is derived from the post id (accurate to within seconds). Always null here: like_count, comment_count, video_url, is_pinned, location; tagged_users and children are empty. Use the post endpoint below for all of them (one request per post).Post with comments
One post's full record, plus one page of its comments (about 14, in Instagram's own "popular" order) and the cursor of the next comment page.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
post | string | Required | — | A shortcode (CrgT1jPISz5) or an instagram.com /p/, /reel/ or /tv/ URL. |
username | string | Optional | null | The owner you expect. If the post belongs to someone else the job fails with owner_mismatch and no comments are fetched. |
include_comments | bool | Optional | true | false fetches the post's metadata only (one request). |
comments_cursor | string | Optional | null | The next_comments_cursor of the previous page's answer. Omit or send null for the first page. Max 2,000 chars. |
A comment_pages field is refused with 400: the API pages by cursor, one page per job.
Poll for results
GET /api/v1/social-scraper/instagram/post/{job_id}/
post is the full record: every grid field filled, plus owner.full_name / is_verified, is_paid_partnership, coauthors (usernames), accessibility_caption, play_count on a video — and the rest of what Instagram's record carries:
| Field | What it holds |
|---|---|
width, height | The original media dimensions in pixels; each carousel child carries its own. |
has_audio | Whether a video has a sound track (null on a photo). |
video_duration | Seconds, when Instagram sends it to a logged-out visitor (null on every reel measured so far). |
view_count | Video views as a separate figure from play_count; null when Instagram withholds it. |
comments_disabled | The owner turned comments off (null when Instagram does not say). |
like_and_view_counts_disabled | The owner hides like and view counts — when true, expect like_count null. |
repost_count | How often the post was reposted, when sent. |
carousel_media_count | Number of children on a carousel. |
top_likers | Usernames Instagram shows as "liked by …". |
sponsor_tags | Usernames of the brands a paid partnership tags. |
audio | A reel's track: {type, title, artist, username} — original_sounds with the creator's username, or licensed_music with the song and artist; null on anything that is not a reel. |
width, height, has_audio and video_duration.comments_total is the post's own comment count. replies_preview holds the first replies to a comment when Instagram sends them (same shape, without replies_preview); reply_count is the full number. Comments come in Instagram's own ("popular") order, about 14 a page; a comment cursor is a JSON string Instagram hands back, passed through verbatim. The metadata and the comment page are separate: comments_truncated true with a truncated_reason means the post's record is in post but the comment page was not served — the job still completes.
Errors — as for profile posts, plus owner_mismatch and post_not_found on the job itself.
Cursors and truncation
Both endpoints page with opaque cursors, one page per job. Every answer carries the cursor of the next page in next_cursor / next_comments_cursor; on the last page it is null and has_more / comments_has_more is false. To get the next page, submit a new job with the cursor — each page is its own session on its own network exit, which is also how you continue after a wall. Cursors are Instagram's and they expire; one that no longer works comes back as login_required with nothing fetched — start again without a cursor.
Read complete first. It answers one question: did you get the page you asked for? When it is false (or null on a failed job), result.truncated_reason says why.
truncated_reason | complete | Meaning | What to do |
|---|---|---|---|
null | true | The page was served. next_cursor is the next page, or null on the last. | Continue with the cursor, or stop. |
login_required | false | Instagram answered a login wall (or a checkpoint) in front of this page. | Retry later with the same cursor. |
rate_limited | false | Instagram answered HTTP 429. | Retry later with the same cursor. |
private | false | The profile is private; anonymous access sees no posts. | None; needs the owner's approval. |
error | false | Something other than a wall: every network exit failed, or Instagram answered an error. error_detail says what. | Retry with the same cursor. |
Within one job a wall is already retried, on a fresh network exit and browser identity, up to twice before the job stops — so login_required means three different exits were walled in a row on that page. A wall in front of a profile page leaves nothing to return, so that job is failed with login_required or rate_limited; it is still one unit.
Error codes
error_message on a failed job starts with one of these; result.error carries it alone and result.error_detail the explanation.
| Code | Meaning |
|---|---|
profile_not_found / post_not_found | Instagram knows no such username / shortcode. |
owner_mismatch | The post belongs to another account than the username you sent. |
login_required / rate_limited | Walled before anything was fetched. |
proxy_unavailable | No network exit is configured for Instagram on the server; the job did not run. |
proxy_fault | Every network exit failed at the transport level. |
instagram_error | An unexpected answer from Instagram (a 5xx, a malformed body). |
instagram_disabled | The feature was switched off while the job was queued. |
crash | An internal error. The job is still billed and reported. |
Rate limits and quota
failed. Limits are shared with the rest of the API, per client — see the Overview. A submit that is refused with 403 or 400 is never charged; only an accepted job (202) is. Instagram jobs count toward the same GET /usage/ totals as other jobs and are not itemised by product there. Each job reports its bandwidth_kb.| Limit | Value |
|---|---|
| Posts per page | 12 (Instagram's size) |
| Comments per page | About 14 (Instagram's size; top-level comments only, replies as a preview) |
| Pages per job | 1 — page with the cursor |
| Cursor length | 2,000 characters |
| Pagination depth measured | 100 pages (1,200 posts) on one profile without a wall, 24 Oct 2026 |
| Wall retries within a job | 2, each on a fresh exit and identity |
| Cache | Post metadata 1 h, first comment page 15 min, profile id 24 h — repeating a request inside that window is cheap |
Access limits — read before integrating
- Logged-out view only. Private profiles return no posts. Instagram meters anonymous reads and walls them after a budget it does not publish; a job that meets a wall tells you so and keeps its cursor, so plan to page with cursors and retry a walled page later.
- Counts and dates per post need the post endpoint. The profile grid carries no like or comment counts, no video URL and no tags;
taken_atis derived from the media id. - Counts are as a visitor sees them.
like_countis null where likes are hidden;comments_totalmay count replies differently fromcomment_count. - Media URLs expire.
display_urlandvideo_urlare signed CDN links valid for a limited time. Download what you need on receipt. - Instagram changes its web client without notice. A job that lands during a change may come back
login_requiredwith nothing fetched; retry later.
Example: Python
Looking for the product overview? See Instagram Scraper API.