framestash

Instagram API

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.

Session values

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.

GraphQL model

Queries Framestash uses

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”

Record shape

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.

Picking the rendition

video_versions if present, else image_versions2.candidates. The largest width × height wins; the first wins ties. No DASH, no separate audio, no muxing.

Profile listing

Incremental re-runs

Cursor

Stories and highlights

ID formats

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

Headers

Call Headers Framestash sets
POST /api/graphql content-type, x-csrftoken, x-ig-app-id, x-fb-lsd
CDN media bytes none

Rate limiting and block signals

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:

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.

Not usable

Lessons