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 clientPooled 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.
| Param | Type | Required | Description |
|---|---|---|---|
username_or_url | string | yes | Instagram 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.
| Param | Type | Required | Description |
|---|---|---|---|
username_or_url | string | one of username_or_url/user_id | Instagram username or full profile URL |
user_id | string | one of username_or_url/user_id | Instagram numeric user ID |
cursor | string | no | Pagination token (max_id) |
count | int | no | Items 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.)
| Param | Type | Required | Description |
|---|---|---|---|
user_id | string | yes | Instagram numeric user ID |
username_or_url | string | no | Username used only for the referer |
include_chaining | bool | no | Include suggested-user chaining |
include_reel | bool | no | Include active story/reel container metadata |
include_suggested_users | bool | no | Include suggested users |
include_logged_out_extras | bool | no | Include logged-out extras |
include_live_status | bool | no | Include live status |
include_highlight_reels | bool | no | Include highlight reel metadata |
GET /instagram/oembed
Fetch oEmbed metadata for a public post or reel.
| Param | Type | Required | Description |
|---|---|---|---|
url | string | yes | Public Instagram post/reel URL |
maxwidth | int | no | Width hint |
maxheight | int | no | Height 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.
| Param | Type | Required | Description |
|---|---|---|---|
code | string | one of code/media_id | Post shortcode or full post URL |
media_id | string | one of code/media_id | Numeric 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).
| Param | Type | Required | Description |
|---|---|---|---|
code | string | yes | Post shortcode or URL |
cursor | string | no | Pagination cursor from previous response |
count | int | no | Comments 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.
| Param | Type | Required | Description |
|---|---|---|---|
code | string | yes | Post 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).
| Param | Type | Required | Description |
|---|---|---|---|
username_or_url | string | one of username_or_url/user_id | Username; with only user_id it is resolved via profile_graphql |
user_id | string | one of username_or_url/user_id | Instagram numeric user ID |
detail | string | no | full (default): full media via one extra timeline call. summary: reels-tab entries only (12KB) |
cursor | string | no | Pagination cursor (paging_info.max_id) |
count | int | no | Accepted 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).
| Param | Type | Required | Description |
|---|---|---|---|
location_id | string | yes | Numeric location ID |
slug | string | no | Location slug used only for the referer |
show_nearby | bool | no | Include 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.
| Param | Type | Required | Description |
|---|---|---|---|
location_id | string | yes | Numeric location ID |
slug | string | no | Location slug used only for the referer |
tab | string | no | ranked or recent |
cursor | string | no | Pagination cursor |
count | int | no | Items 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.
| Param | Type | Required | Description |
|---|---|---|---|
keyword | string | yes | Hashtag/search term or full /popular/{keyword}/ URL |
media_count | int | no | Media items per page (1-50, default 12) |
related_keywords_count | int | no | Related keyword suggestions to return (0-50, default 10) |
include_description | bool | no | Include 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:
| Field | Value |
|---|---|
session_id | instagram:pool:instagram_evomi_core_residential:N |
cookies | csrftoken, mid, ig_did, datr, ps_l, ps_n, wd, ig_nrcb |
proxy_session_id | Sticky proxy identity (same exit IP per session) |
metadata.fp | Full IgFingerprint (screen_size, web_session_id, etc.) |
metadata.asbd_id | Extracted from Instagram responses |
Pool lifecycle:
- Startup: creates the idle pool without proxy traffic
- Request: allocates → uses → releases or quarantines
- Isolation: the cookie jar is scoped to the proxy spec; provider failover is disabled
- 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: 1800Fingerprint Details
The IgFingerprint dataclass generates realistic device identity:
| Field | Source | Description |
|---|---|---|
ig_did | UUID v4 | Device ID cookie |
csrf_token | secrets.token_hex(16) | CSRF token (cookie + header) |
mid | secrets.token_urlsafe(21) | Machine ID cookie |
web_session_id | 3x token_hex(3) | Web session ID header |
screen_size | random from pool | Viewport dimension |
datr | extracted from response | Browser ID cookie |
lsd | extracted from JS | GraphQL anti-CSRF token |
jazoest | extracted from response | Comet POST anti-CSRF token |
haste_session | extracted from response | Comet SiteData field |
hsi | extracted from response | Comet SiteData field |
spin_r/spin_b/spin_t | extracted from response | Comet SiteData build/revision fields |
server_revision | extracted from response | Comet 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 bodylsd:"LSD",[],{"token":"<value>"}in bodyasbd_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
| Variable | Default | Description |
|---|---|---|
IG_APP_ID | 936619743392459 | Instagram app ID header |
IG_ASBD_ID | 359341 | ASBD ID fallback |
IG_POST_DOC_ID | 25951076901259180 | GraphQL doc ID for post_simple (kept-but-unrouted fallback) |
IG_COMMENTS_DOC_ID | 27261273046856309 | GraphQL doc ID for comments |
IG_HIGHLIGHTS_QUERY_ID | 9957820854288654 | GraphQL query ID for story/live/highlight status |
IG_TIMELINE_QUERY_ID | 27538170685818452 | GraphQL query ID for timeline: backs /user/posts and reels enrichment |
IG_PROFILE_ROOT_DOC_ID | 27981003384861049 | GraphQL doc ID for user_id (logged-out profile root) |
IG_REELS_TAB_DOC_ID | 27838951732404191 | GraphQL doc ID for reels (reels tab, play_count) |
IG_POST_ROOT_DOC_ID | 27130156389949648 | GraphQL doc ID for post (post + first-page comments) |
IG_POPULAR_SEARCH_DOC_ID | 26300727659606513 | GraphQL doc ID for popular/hashtag search |
IG_PROFILE_CONTENT_DOC_ID | 28036671149327607 | GraphQL doc ID for profile_graphql — backs /user/summary and the /user/info?detail=summary fast path |
IG_SESSION_TTL | 1800 | Session TTL (seconds) |
IG_WARMUP_TIMEOUT_MS | 15000 | Warmup 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.