toolzer Internal Docs
WorkerSites

Instagram — Worker-Internal Reference

Worker-internal routes for Instagram public data. These are not the public API routes — they are called by the API layer (apps/api) via HTTP. Clients never call these directly.

Architecture

Client -> API layer -> GET /instagram/user/info?username_or_url=zuck
  -> SiteRouteEngine
    -> SessionPool.allocate("instagram")
    -> InstagramAdapter.build_fetch_request()
      -> Builds Instagram-specific URL, headers, GraphQL body
      -> Attaches IgFingerprint cookies (ig_did, csrftoken, mid)
      -> Returns FetchRequest with pool-owned session_id
    -> StatefulFetcher.fetch()
      -> Redis: load pooled SessionBundle
      -> HttpFetcher: TLS-impersonated HTTP request via residential proxy
      -> Block detection: "challenge_required", "login_required"
    -> ON SUCCESS:
      -> SessionPool.release(session_id)
      -> Save response cookies + extracted tokens
    -> ON BLOCK:
      -> SessionPool.quarantine(session_id)
      -> Background spawns replacement
    -> InstagramAdapter.transform_response()
      -> Extracts datr, lsd, asbd_id, csrftoken, mid
  -> Response to client

Pooled sessions carry their own sticky identity, cookies and fingerprint, under a shared 260-request concurrency key. The client asks for data and never leases Instagram sessions directly.

Two routes exist in routes.py but have no section below: GET /instagram/user/id (resolve a username to numeric ID, cheaper than /user/info) and GET /instagram/user/summary (condensed profile by numeric ID, one physical data attempt). The summary route does not retry after a tunnel attempt. Both are real, live routes — see routes.py and schemas.py directly.

Endpoints

GET /instagram/user/info

Look up an Instagram user by username. If the feed cannot resolve the ID, summary mode reads it from the mobile page and attempts the compact query once. A successful compact query supplies the complete summary; otherwise the mobile profile stays partial. Full mode keeps partial markers when only summary fields are available.

An active profile-not-found page returns null, including when Instagram serves that page with HTTP 200. Empty or login-gated pages are not evidence of deletion. The same recovery applies to ID lookup. See the dated investigation.

ParamTypeRequiredDescription
username_or_urlstringyesInstagram username or full profile URL
curl "http://localhost:8000/instagram/user/info?username_or_url=zuck"

GET /instagram/user/posts

Fetch a public account's own posted media (photos/videos/reels mixed). Accepts either identifier — pass user_id when known, since it's more stable than a username that can change.

ParamTypeRequiredDescription
username_or_urlstringone of username_or_url/user_idInstagram username or full profile URL
user_idstringone of username_or_url/user_idInstagram numeric user ID
cursorstringnoPagination token (max_id)
countintnoItems per page (1-50, default 12)

Backed by the logged-out timeline GraphQL query (timeline, by numeric id); a username is resolved first through user_id. The response keeps the retired REST feed's items / more_available / next_max_id shape. See research/instagram/21-logged-out-rest-rollout-2026-10.md.

GET /instagram/user/highlights

Fetch a user's active story presence, live-broadcast status, and highlight reel metadata. (Not additional profile fields — despite the upstream GraphQL operation being named "profile extras", the payload is about story/live/highlight state.)

ParamTypeRequiredDescription
user_idstringyesInstagram numeric user ID
username_or_urlstringnoUsername used only for the referer
include_chainingboolnoInclude suggested-user chaining
include_reelboolnoInclude active story/reel container metadata
include_suggested_usersboolnoInclude suggested users
include_logged_out_extrasboolnoInclude logged-out extras
include_live_statusboolnoInclude live status
include_highlight_reelsboolnoInclude highlight reel metadata

GET /instagram/oembed

Fetch oEmbed metadata for a public post or reel.

ParamTypeRequiredDescription
urlstringyesPublic Instagram post/reel URL
maxwidthintnoWidth hint
maxheightintnoHeight hint

GET /instagram/post

Fetch a post's full public media object (caption, image/video variants, counters) plus its first page of comments. Accepts either identifier — pass media_id when known; code is converted to a numeric media ID locally (no extra request) via the standard Instagram shortcode encoding.

ParamTypeRequiredDescription
codestringone of code/media_idPost shortcode or full post URL
media_idstringone of code/media_idNumeric media ID
curl "http://localhost:8000/instagram/post?code=CxQl2oQO__S"

A simpler GraphQL mechanism (post_simple operation, no comments) exists in the adapter as a kept-but-unrouted fallback.

GET /instagram/post/comments

Fetch paginated comments for a post (use after /instagram/post for pages beyond the first).

ParamTypeRequiredDescription
codestringyesPost shortcode or URL
cursorstringnoPagination cursor from previous response
countintnoComments per page (1–50, default 50)
curl "http://localhost:8000/instagram/post/comments?code=CxQl2oQO__S&count=20"

GET /instagram/post/views

View counts for one post or reel. video_view_count comes from the embed page; play_count from the owner's Reels tab (newest 36 reels only, else null). They are different metrics. Photo, private or deleted posts return null.

ParamTypeRequiredDescription
codestringyesPost shortcode or full post URL
curl "http://localhost:8000/instagram/post/views?code=DePb6WLuVae"

GET /instagram/user/reels

Fetch a user's reels with pagination. Every item carries play_count (the plays shown on the profile's Reels tab).

ParamTypeRequiredDescription
username_or_urlstringone of username_or_url/user_idUsername; with only user_id it is resolved via profile_graphql
user_idstringone of username_or_url/user_idInstagram numeric user ID
detailstringnofull (default): full media via one extra timeline call. summary: reels-tab entries only (12KB)
cursorstringnoPagination cursor (paging_info.max_id)
countintnoAccepted 1–50; Instagram serves 12 per page
curl "http://localhost:8000/instagram/user/reels?user_id=42526505485&count=12"

GET /instagram/location/info

Fetch public location metadata, read from the location page's server-rendered Relay prefetch (the XHR query is rate-limited logged out).

ParamTypeRequiredDescription
location_idstringyesNumeric location ID
slugstringnoLocation slug used only for the referer
show_nearbyboolnoInclude nearby locations

GET /instagram/location/media

Deprecated (2026-07-25). Live-checked against real high-traffic locations (both tabs, with and without a cursor) and it consistently returns empty edges — see research/instagram/12-location-graphql.md. Left wired up for internal/testing use; do not build new callers against it.

Fetch public location media tab data.

ParamTypeRequiredDescription
location_idstringyesNumeric location ID
slugstringnoLocation slug used only for the referer
tabstringnoranked or recent
cursorstringnoPagination cursor
countintnoItems per page (1-50, default 6)

GET /instagram/search/popular

Fetch public media, related keyword suggestions, and an AI-generated topic description for a hashtag/keyword.

ParamTypeRequiredDescription
keywordstringyesHashtag/search term or full /popular/{keyword}/ URL
media_countintnoMedia items per page (1-50, default 12)
related_keywords_countintnoRelated keyword suggestions to return (0-50, default 10)
include_descriptionboolnoInclude the topic description (default true)
curl "http://localhost:8000/instagram/search/popular?keyword=travel"

Session Pool

Each pooled session stores a complete browser identity in Redis:

FieldValue
session_idinstagram:pool:instagram_evomi_core_residential:N
cookiescsrftoken, mid, ig_did, datr, ps_l, ps_n, wd, ig_nrcb
proxy_session_idSticky proxy identity (same exit IP per session)
metadata.fpFull IgFingerprint (screen_size, web_session_id, etc.)
metadata.asbd_idExtracted from Instagram responses

Pool lifecycle:

  1. Startup: creates the idle pool without proxy traffic
  2. Request: allocates → uses → releases or quarantines
  3. Isolation: the cookie jar is scoped to the proxy spec; provider failover is disabled
  4. Scaling: all replicas share one Redis concurrency ceiling

Instagram runs on one tier, evomi.core_residential, declared in sites.yaml. The worker rejects /pool/instagram/lease; only the site routes own these sessions.

Pool config (sites.yaml):

sites:
  instagram:
    pool_min_idle: 5
    pool_max_idle: 30
    pool_max_total: 320
    max_concurrency: 260
    session_ttl: 1800

Fingerprint Details

The IgFingerprint dataclass generates realistic device identity:

FieldSourceDescription
ig_didUUID v4Device ID cookie
csrf_tokensecrets.token_hex(16)CSRF token (cookie + header)
midsecrets.token_urlsafe(21)Machine ID cookie
web_session_id3x token_hex(3)Web session ID header
screen_sizerandom from poolViewport dimension
datrextracted from responseBrowser ID cookie
lsdextracted from JSGraphQL anti-CSRF token
jazoestextracted from responseComet POST anti-CSRF token
haste_sessionextracted from responseComet SiteData field
hsiextracted from responseComet SiteData field
spin_r/spin_b/spin_textracted from responseComet SiteData build/revision fields
server_revisionextracted from responseComet SiteData field

The last five fields are only populated after a pool warmup fetch (state.py::extract_warmup_metadata) — a fresh session has them empty until its first warmup completes. They're required for Comet POST operations (post, comments); a request without them fails upstream with error 1357005.

Response extraction patterns:

  • datr: cookie value or "_js_datr","<value>" in body
  • lsd: "LSD",[],{"token":"<value>"} in body
  • asbd_id: cookie value or "asbd_id"="<digits>" in body

Block Detection

Status codes 401, 403, 429 and body signatures challenge_required, login_required, checkpoint_required, /accounts/login/, __coig_login=1, Please wait a few minutes, feedback_required, rate_limit, captcha are treated as blocks (adapter.py::_BLOCK_SIGNATURES, 9 signatures total). post_views drops bare captcha: every embed lists arkose_captcha among its CSP hosts. On block, the pooled session is quarantined and a replacement is spawned automatically.

Configuration

VariableDefaultDescription
IG_APP_ID936619743392459Instagram app ID header
IG_ASBD_ID359341ASBD ID fallback
IG_POST_DOC_ID25951076901259180GraphQL doc ID for post_simple (kept-but-unrouted fallback)
IG_COMMENTS_DOC_ID27261273046856309GraphQL doc ID for comments
IG_HIGHLIGHTS_QUERY_ID9957820854288654GraphQL query ID for story/live/highlight status
IG_TIMELINE_QUERY_ID27538170685818452GraphQL query ID for timeline: backs /user/posts and reels enrichment
IG_PROFILE_ROOT_DOC_ID27981003384861049GraphQL doc ID for user_id (logged-out profile root)
IG_REELS_TAB_DOC_ID27838951732404191GraphQL doc ID for reels (reels tab, play_count)
IG_POST_ROOT_DOC_ID27130156389949648GraphQL doc ID for post (post + first-page comments)
IG_POPULAR_SEARCH_DOC_ID26300727659606513GraphQL doc ID for popular/hashtag search
IG_PROFILE_CONTENT_DOC_ID28036671149327607GraphQL doc ID for profile_graphql — backs /user/summary and the /user/info?detail=summary fast path
IG_SESSION_TTL1800Session TTL (seconds)
IG_WARMUP_TIMEOUT_MS15000Warmup fetch timeout

Response Format

No envelope wrapper. On a 2xx upstream response the cleaned/raw payload is the body itself (api/responses.py::ok()). On a blocked or errored upstream response, routes.py::_site_response collapses the status into one of a few meanings and returns a minimal problem object via fail() — the HTTP status line carries the code, not a body field:

Block response (upstream 401/403/429):

{
  "type": "UPSTREAM_BLOCKED",
  "message": "Upstream blocked the request",
  "context": { "upstream_status": 403 }
}

with HTTP status 503 (blocks are always reported as 503, regardless of the upstream's own status code). A 404 from upstream returns null with HTTP 200. Any other non-2xx status is forwarded as UPSTREAM_ERROR with that same status code.

On this page