What Framestash uses to list posts, stories and highlights. None of this is documented or stable; it was checked live in October 2026. Re-check with the playbook (steps 2–5) before relying on it, and update this file when something has changed.
All calls run in the page world (extension/page.js), because some tokens exist only there. The panel never talks to Instagram except to fetch CDN bytes.
The browser sends Instagram’s cookies with same-origin requests, so Framestash never handles login. Per request, page.js reads:
| Value | Source |
|---|---|
doc_id |
require("<Operation>_instagramRelayOperation") |
fb_dtsg |
require("DTSGInitialData").token. Mandatory: without it the server answers with HTML |
lsd |
require("LSD").token, sent in the body and as x-fb-lsd |
| app ID | require("PolarisConfig").getIGAppID() |
| CSRF token | the csrftoken cookie, sent as x-csrftoken |
Nothing is hardcoded or stored. No www-claim header is needed.
POST /api/graphql, form-encoded: doc_id, variables (JSON), lsd, fb_dtsg.doc_id is a persisted-query ID that rotates. Instagram’s module loader exposes it under a stable operation name, so it is read at call time.require("<Operation>.graphql") and sends them as false, as Instagram does.for (;;);, which is stripped before parsing.| Purpose | Operation | Variables | Root field | Notes |
|---|---|---|---|---|
| profile posts | PolarisProfilePostsQuery |
username, first: 12, after, data: {count, …} |
xdt_api__v1__feed__user_timeline_graphql_connection |
edges[].node, page_info {has_next_page, end_cursor} |
| one post | PolarisPostRootQuery |
shortcode |
xdt_api__v1__media__shortcode__web_info |
items[] |
| username → user ID | PolarisAPITopsearchQuery |
data: {query, context: "blended", …} |
xdt_api__v1__fbsearch__topsearch_connection |
match the username exactly, case-insensitive. Works for private accounts and accounts without posts, unlike the profile posts query |
| a profile’s highlights | PolarisProfileStoryHighlightsTrayContentQuery |
user_id, first, after |
highlights |
node IDs look like highlight:<id> |
| stories / highlight items | PolarisStoriesV3ReelPageStandaloneQuery |
reel_ids_arr, is_highlight, media_id: null |
xdt_api__v1__feed__reels_media |
see “Stories and highlights” |
Every node Framestash consumes has the v1 shape, because the GraphQL layer wraps the same backend objects:
| Field | Meaning |
|---|---|
pk |
numeric media key |
id |
<pk>_<owner user ID> |
user.username, user.pk |
owner |
taken_at |
Unix seconds. Carousel children from the profile posts query lack it; Framestash uses the parent’s |
carousel_media[] |
carousel children, each with its own pk, id and media |
image_versions2.candidates[] |
{url, width, height} |
video_versions[] |
{url, width, height}, progressive MP4 with audio |
Post nodes may also carry is_dash_eligible, number_of_qualities and video_dash_manifest; see DASH video.
itemsFromNode treats every node as untrusted: Instagram-shaped usernames and IDs only, and media URLs only on *.cdninstagram.com or *.fbcdn.net.
video_versions if present, else image_versions2.candidates. The largest width × height wins; the first wins ties. No DASH, no separate audio, no muxing.
PolarisProfilePostsQuery lists posts newest first by taken_at. Up to 3 pinned posts come first on page 1, with older taken_at than the posts after them, and may appear again at their date position.first: 12 on a large account; a 6-post profile returned 7 edges, its pinned post twice). Dedup by pk.page_info.end_cursor per user ID after every page, and the next run of that account continues from it.reel_ids_arr: [userId], is_highlight: false.highlight:<id> IDs (50 per page), then the reels query with those IDs, is_highlight: true, in batches of 20.pk and <pk>_<ownerId> IDs, but user is empty on items: take the owner from the reel.id is highlight:<id>, and its title is the highlight’s display name, the label shown under it on the profile (checked live, October 2026).| Name | Example | Notes |
|---|---|---|
pk |
3900000000000000071 |
larger than 2^53: keep it a string |
id |
3900000000000000071_31600000058 |
pk + _ + owner user ID |
| shortcode | DdRKJpXjzGe |
the pk in base 64 over A–Z a–z 0–9 - _, most significant digit first. Strings longer than 28 characters carry a 28-character suffix to remove before decoding |
| user ID | 31600000058 |
stable across username changes |
| reel ID | 31600000058 or highlight:<id> |
a user’s story reel ID equals the user ID |
| Call | Headers Framestash sets |
|---|---|
POST /api/graphql |
content-type, x-csrftoken, x-ig-app-id, x-fb-lsd |
| CDN media bytes | none |
Limits appear to apply per endpoint: the REST endpoints below returned 429 while /api/graphql worked. Framestash paces every call (extension/pacer.js) and treats these as block signals, holding the whole queue instead of retrying:
response.redirected), e.g. to /accounts/login/ or /challenge/…;message: "Please wait a few minutes before you try again.": temporary;message: "feedback_required" with spam: true: action blocked, temporary;message: "checkpoint_required" with checkpoint_url: the user must clear a challenge;message: "login_required" and/or require_login: true: the session is gone;Block replies often come with HTTP 400, so error bodies are read too. Only 429 has been seen live; confirm the others when they occur. Login and challenge holds wait for the user’s Resume.
GET /api/v1/feed/user/<username or id>/…: redirects to / (200 with HTML).GET /api/v1/users/web_profile_info/: 429, even on a fresh session.GET /api/v1/media/<id>/info/: 429 at times.GET /graphql/query/?query_hash=…: legacy, not used by Instagram’s UI.200 doesn’t mean it worked. Check status, redirected and the body before trusting it, and report “HTTP 429” or “unexpected HTML”, never a parser error.pk before naming files.