Getting Started
Read public Instagram profiles, posts, reels, comments and trends as structured JSON. No Instagram account and no developer application — just your FetchLayer key.
1. Get your API key
Create an account and generate a FetchLayer API key from your dashboard. The same key works on every platform.
Create free account →2. Read an account
Pass a handle with or without the @, or a profile URL. The bio links come back unwrapped to their real destinations, and the story highlights come back with them.
curl -X POST https://api.fetchlayer.dev/instagram/user-profile \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"username":"@nasa"}'Story highlights come free with the profile
highlights lists the id, title and cover of every highlight on the profile, in the same call and for the same credit. Their contents are not public — only the titles and covers are. Exact counts, and where an account is based
stats.postCount is the exact number of posts, private accounts included, alongside the follower and following counts. hasActiveStory and latestStoryAt say whether a story is up now and when the last one was posted. Add "include":["country"] to also get country, where the account is based, and dateJoined, the month it joined; it adds a second or two. 3. List what they posted
Newest first, pinned posts first, each normally with its posting date, like and comment counts, and for a video its play count and length — plus captions, hashtags, mentions and every rendition.
curl -X POST https://api.fetchlayer.dev/instagram/user-posts \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"username":"@nasa","limit":60}'Twelve posts per page, and check fieldsFrom
limit says, so limit: 60 takes five pages and costs five credits, and without a limit every post on the pages fetched comes back (pages: 2 returns 24) — pagesFetched reports it. Posts come with their dates and counts and say fieldsFrom: "listing_full". A post still missing one of them is named by short code in notes, with the fields it lacks; asking again with include returns them, and /post-detail fills the gap too — so a null is never mistaken for a zero. 4. Read the reels, with play counts
Same shape as the posts listing. Every reel comes back with playCount, likeCount and commentCount, and most with their posting date, length and caption too.
curl -X POST https://api.fetchlayer.dev/instagram/user-reels \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"username":"@nasa","limit":12}'Read what they have up now
The stories an account has up right now, oldest first, each frame with its posting and expiry time, every rendition, any link the account attached and any post it re-shared.
curl -X POST https://api.fetchlayer.dev/instagram/user-stories \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"username":"@nasa"}'No story is an answer, not an error
stories list and a note; a private account returns stories: null and a note. Both cost 1 credit, like any other call. A call usually takes 2–5 seconds, and a 503 under load is worth retrying. 5. Open one post, and read its comments
By URL, by the short code from it, or by the post’s numeric id. A video or reel comes back with its current play count and its length in seconds; photos and carousels have no public view count. Add "include":["reposts","shares","saves"] to also get repostCount, shareCount and saveCount — reposts on any post, shares and saves on reels and videos. It adds about 1 to 2 seconds, and the same include works on /user-posts and /user-reels for a whole page at the same price — there it adds a few seconds per page, usually 2 to 8.
curl -X POST https://api.fetchlayer.dev/instagram/post-detail \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"post":"https://www.instagram.com/p/DcOX3hWFiey/"}'
curl -X POST https://api.fetchlayer.dev/instagram/post-comments \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"post":"DcOX3hWFiey"}'Comments page, a page at a time
pages (default 1) returns several pages in one request and cursor continues from a nextCursor; each page costs one credit. The response can be shorter than asked and then carries a nextCursor and a note. replyCount is a number on every page; on a first page some reply counts and the photos of photo comments can be empty, the note says so, and asking again fills them. To read the replies to a comment, pass its id as commentId to /comment-replies: about 8 replies a page, oldest first, 1 credit per page. A request for several pages can take up to about 20 seconds between pages, so follow nextCursor one page at a time. 6. Discovery, without search
Instagram does not make keyword search or hashtag feeds public. What it does publish is the reels it lists as popular and the terms it is promoting. /reels-feed returns popular reels from many accounts, varying between calls, each with its play count, caption, author and video; like counts, comment counts and dates come from /post-detail.
curl -X POST https://api.fetchlayer.dev/instagram/reels-feed \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"limit":12}'
curl -X POST https://api.fetchlayer.dev/instagram/trending-searches \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"limit":48}'Trending is not search
/trending-searches returns the terms Instagram itself is promoting, grouped into its own sections — currently seven, from “Trending now” to “Shop the trends”, named in notes — each with Instagram’s rounded count of posts under it. It is what Instagram is putting in front of everyone — not a query you control.
7. Assets that keep working
Instagram’s asset URLs are signed and expire, so every asset carries a downloadUrl pointing at GET /instagram/media, which re-fetches it on demand with Range support.
curl -L "https://api.fetchlayer.dev/instagram/media?url=<asset url>" \
-H "Authorization: Bearer ss-your-key" \
--output post.jpgResolving a pasted link
When someone hands you an Instagram URL and you do not know what it points at, /resolve-url tells you — post, reel, account, hashtag, place, story or explore — plus the short code, numeric id, handle, tag or place id it encodes. It reaches Instagram not at all, so it is free.
curl -X POST https://api.fetchlayer.dev/instagram/resolve-url \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.instagram.com/p/DcOX3hWFiey/"}'A link naming something Instagram does not make public — a hashtag page, a place — is still identified, with a note saying it is not available. That is the cheapest way to find out. A share link (/share/…) does not say which post it points to, so it comes back as unknown with a note to open it and pass the link it lands on.
Errors and retries
A 400 names the field to fix; unrecognised fields are rejected rather than ignored. A 404 means there is no such account or post — {"error":"no account \"fetchlayerprobe9f3\" was found","status":404} — never a 503 dressed up as a failure. An account Instagram restricts, typically an age-restricted brand, is not a 404: it answers 200 with restricted: true and less detail. A 502 or 503 is worth retrying after a wait. Read notes on successful responses too: the comment ceiling, the counts Instagram does not make public and a private account all announce themselves there rather than as an error.
Error responses
Every error, on every route, is JSON in one shape: error (what went wrong, in words), status, code, retryable and requestId, which also comes as the X-Request-Id header. Branch on code and retryable rather than on the text; on a 5xx the text is generic. Quote requestId when reporting a problem.
{
"error": "Missing required parameter: url",
"status": 400,
"missing": ["url"],
"code": "invalid_input",
"retryable": false,
"requestId": "922874e3-c0d0-40d8-8866-d198af132420"
}| code | Status | Retryable | Meaning |
|---|---|---|---|
| invalid_input | 400 | no | The request needs fixing; error names the field. A missing required parameter also lists it in missing. |
| unauthorized | 401 | no | The API key is missing or not valid. |
| insufficient_credits | 402 | no | Nothing left to spend: no credits, a used-up free trial, or a monthly allowance and credit balance both spent. error says which. Waiting does not help; topping up does. |
| forbidden | 403 | no | This key or account is not allowed this request. |
| not_found | 404 | no | What the request names does not exist or is not public. |
| expired | 410 | no | What the request refers to is no longer available; ask for it afresh. |
| quota_exceeded | 429 | no | A request-rate limit was reached; error says which. Slow down before sending more. |
| cancelled | 499 | no | The caller closed or cancelled the request before it finished. The work was stopped and nothing was charged. |
| time_limit | 503 | yes | Your timeoutMs ran out before there was anything to return; ask again, or allow more time. |
| temporarily_unavailable | 503 | yes | Not available right now; ask again shortly. |
| posts_unavailable | 403 | no | TikTok user videos: TikTok is not showing this account’s posts publicly right now. Asking again at once returns the same answer; nothing is charged. |
| store_unreachable | 503 | yes | Store routes (Shopify, WooCommerce): the store itself can’t be reached right now — it may be offline. Ask again later. Where a store route doesn’t say so yet, an offline store is temporarily_unavailable. |
| upstream_error | 502 | yes | The source answered in a way that could not be used; ask again later. |
| internal_error | 500 | yes | Something failed on our side; ask again later, and quote requestId if it persists. |
| invalid_cursor | 400 | no | The cursor is not one this route returned for this account; start again without it. |
| tool_error | — | no | MCP only: a tool call failed in a way no other code describes; error says what. |
More codes may be added; treat one you do not recognise by its retryable flag. Over MCP, a tool error carries the same fields in structuredContent, and as a JSON text block beside the message.