Appearance
Changelog
Versions follow the rule in CONTRIBUTING.md: a major version only for large changes to the Simulator's own core; a breaking change forced by Instagram's remote API is a minor version; a patch is a fix.
Unreleased
The product is named igpapi, its tagline is Instagram private API Simulator for Node.js (in prose, the Simulator); the package names are unchanged.
Documentation: a new guide, and a site found by "instagram private api"
The documentation is rebuilt. Start with Your first program: install, log in, save the session and like a post in about 15 minutes. New pages explain how the Simulator works, how to get a code that arrives during a run, account safety and pacing, the effects that have no screen, the glossary and every recipe. The screen pages follow one template, with their screenshots where they show a state, and the errors page lists every typed error with what to do. The API reference is grouped by what a name is for, and every page of the site is also served as llms.txt and llms-full.txt for coding agents. The product tagline is "Instagram private API Simulator for Node.js", and the home page explains that a Simulator is not an emulator. The iOS package is not the priority direction: it is implemented less exhaustively than the Android one.
- Added:
@igpapi/androidand@igpapi/iosre-exportSnapshotDecodeError,CheckpointDecodeError,PlainJsonApiHttp,JsonResponseHttpandawaitMobileProcessBackground(Android alsoEnterAppBackgroundandEnterAppForeground), so a project that installs only the platform package resolves them under pnpm and Yarn Plug'n'Play.FeedTimelineReelItemis exported like the other item definitions,isSearchInputUserResult(row)narrows a row of the search field's suggestions to a user, andPostingStoryNotPublishedErroris exported with the other story publication names.
Added: an answer in flight across a push
A screen's enter can send a request and go on while it is in flight, and the screen it pushes next takes the answer instead of sending the request itself. The answer is given once; a failure of the request, a challenge included, is the taking screen's enter's failure; an answer no screen takes is discarded, and a session-fatal one ends the process with its typed error.
- Added:
answerAcrossPush(with its typeScreenAnswerAcrossPush) in@igpapi/core/mobile, and thesendAcrossPushandtakeAcrossPushmembers of every screen'senterarguments.
Fixed: the Pigeon upload is the app's, and its answer is read
The telemetry upload (/pigeon_nest) now has the app's envelope byte for byte, and the library reads the upload's answer into the state the app keeps from it (packages/pigeon-nest/spec/).
- The envelope. The flags are
[0, 1, 2, 3, 4]; every batch ends with field 19; the build code in field 11 is written as the unsigned zigzag varint the protocol says (@igpapi/thriftwrote a negative number for an i32 of 2^30 or more and a decoder read16191913back; the writer and the legacy reader are fixed); the second slot of the device is the family device id (it carried the client session id); the upload carries the identity request until the server named the identity in the process, and the sampling and config checksum entries of the identity; the viewer's tokens arehmac.…,at.…and thehmac_ttltoken the server issued; theUFS-<uuid>-<n>counter follows the process's foreground moments (n = max(fc − 1, 0)), in the batch and inx-pigeon-session-idalike. - The answer. The sampling and config checksums and the
hmac_ttltoken an answer carries are kept inMobileDeviceState.pigeonIdentities(in the snapshot, by viewer) and echoed by the next upload. The outcome of a delivered upload (AnalyticsSendOutcome) carries the answer's text (answer). An answer item the logger cannot apply is logged as a warning and counted, never dropped silently. @igpapi/thriftreader fixes.BufferReaderdecoded everyi64wrongly (zigzagToBigint: 0 read as -1) and clamped a read past the end of its input to the last byte; the zigzag of ani32of 2^30 or more was read negative. A read past the end is aThriftError;BufferWriterwrites a whole number or the reader's{ int, num }as ani64instead of calling methods it never had.- Added:
MobileDeviceState.pigeonIdentities,AnalyticsSendOutcome.answerandMobileAppLifecycleState.foregroundCountin@igpapi/core/mobile. - Breaking (
@igpapi/pigeon-nest, for a caller that builds aPigeonContextitself):sessionIdisfamilyId;hmacTokensisauthTokens(thehmac_ttltoken ishmacTtlToken);identityRequest,samplingChecksumandconfigChecksumare required;pigeonInitrecords the session and sends no empty upload (the app sends none), sopigeonSendInfraBatchesbegins with its first batch of events;PigeonChecksum.timestampis the text of thei64. A platform that provides its ownMobileAppLifecycleaddsforegroundCount(its count of foreground moments); ending the named Pigeon identities at a moment is the core's, so the interface has nothing more to implement. Migration: rename the two fields, passidentityRequest: falseand{ timestamp: '0', hash: '' }for the two checksums when the upload is not a session's own, write a checksum'stimestampas a string, and return the number of foreground moments of your process (0when it models none) fromforegroundCount.
4.14.0 (2026-10-28)
The product is named igpapi — Instagram Simulator (in prose, the Simulator); the package names are unchanged.
Documentation: the README files are a guide with a site
The root README is a short landing page, and its topics are pages of their own under guide/ (install, quick start, log in with two-factor, keep a session, checkpoints and restores, ways to start the app, the launch, otherwise, errors, upgrading and the FAQ, with the recipes). Each screen page now says what the person sees, what you do and what you get, with a program that runs it. The pages are built into a documentation site, with the API reference generated from the source under /api/. The iOS package is not the priority direction: it is implemented less exhaustively than the Android one.
Changed: an Android device registers push by itself
The Android device obtains its own push token, as the phone's own push service does: it creates its FBNS key once, keeps it in the snapshot (MobileDeviceState.fbnsKey), and registers the token computed from it. It registers after every login, and at a start on a stored session when its last registration (MobileDeviceState.lastPushRegistration) is more than five hours old. The registration reports the device's notification permission (MobileDeviceState.hasNotificationPermission; a device that declares none has none, as a fresh install). A device restored from an earlier release that registered your token counts as registered, so its next login registers as the app does after one.
- Breaking: the Android
pushTokenoption is removed (AndroidIgpapiOptions.pushToken, soAndroidApp({ pushToken }),AndroidIgpapi.install({ pushToken })andlaunch({ pushToken })). An Android device registers its own token; a caller's token is never sent. Migration: delete the option; passing it is a type error. iOS keepspushTokenfor the APNs token Apple issues. - Breaking:
AndroidPushRegisterandAndroidPushRegisterInputare no longer exported from@igpapi/android. The push registration is a step of the launch that the device runs by itself; a program that calledAndroidPushRegister({ pushToken })sent a registration the app sends only at a login or a start. Migration: delete the call; the launch registers the device.
Changed: every JSON text is read by the core's one reader
A number of more than 15 digits can arrive in any field of any answer; the library now reads every JSON text with the core's one reader, which keeps such a number as its exact text, and the packages outside the core no longer parse JSON themselves.
BloksResponsecarries the parsed body.json(the body after the tolerant repair;undefinedwhen the body is not JSON) andjsonLiterals(the string literals that are JSON objects, parsed) are lazy, read once and not enumerable. ABloksResponseyou build yourself must provide them (createBloksResponseandbloksResponseOfdo).Bloks.parseJSONreads a number of more than 15 digits as its exact text (it did so only with{ longIds: 'string' }, which is removed: the option is gone and the behaviour is the default).Bloks.collectJsonLiteralsdoes the same for the literals it returns.- Migration: a number you took from
parseJSONorcollectJsonLiteralsthat has more than 15 digits is now a string; read it as text (an id is text). - Breaking: an id is always a string, whatever its length.
LoginResponse.logged_in_user.pkandMediaOembedResponse.author_idarestring(they werenumber; an id of more than 15 digits could not be a number without losing digits). The core'sIdAsTextis the schema of such a field. Migration: stop callingNumber(pk)and compare ids as strings (pk === '555', notpk === 555). - Breaking: every id is a
string, in every response type and every input. An id that named an entity (a user, a media, a comment, a thread, an item, a story, a location, a hashtag, an audio, a broadcast) was anumberin about 340 response types, orstring | numberin the inputs and some responses; it isstringeverywhere now, whatever its length, and the library reads the answer's number as the text of that number (the core's one JSON reader, in the mode every server answer is read with, turns the numbers under id keys —pk,id,*_id,*_ids,fbid,cni— into text withIdAsText, so a plain answer, a streamed one, an error body, a Bloks or GraphQL body and a screen item's raw node all give ids as text; an id of more than 15 digits was text already). What the library sends is unchanged: the form field, path or JSON number the app sends is the same bytes; where the app writes an id as a JSON number (the QuickPromotionqp_id/qp_nux_id, the upload settings'target_id, UFAC'sexternal_flow_id/enrollment_id) the library writes it as a number from the id's exact digits. Counters (seq_id), telemetry markers (markerId,instanceId) and enumerations (sim_carrier_id) are not ids and stay numbers.- The changed types: the
pk,id,user_id,*_id,fbid_v2,interop_messaging_user_fbid,facebook_places_id,next_max_id,audio_asset_id,admin_user_ids,effect_ids… of the responses ofaccount(ChallengeStateResponse,RecoveryLookupByUsernameResponse, the login, logout, profile-picture, username and security answers),activityandnewsinboxes,address-book,clips(ClipsHomeFeedResponseRootObject,ClipsMusicFeedResponseRootObject,ClipsUserFeedResponse),collections,direct(the inbox, thread, pending-inbox, ranked-recipients, create-group, add-user, update-title, approve-participant-requests and get-by-participants responses),discover(TopicalExploreFeedResponse),friendships(followers, following, pending, besties, private stories),highlights,igtv(browse, channel, search),live(create, archive, post, viewer list, comments, questions, final viewers),location(LocationInfoResponse,LocationMediaFeedResponse,LocationStoryResponse),login.nativeandLoginTwoFactorRequiredResponse(two_factor_info.pk),media(info, edit, comments, liked, saved, only-me, products tagging, story poll vote, likers),posting(photo, clip, story photo and video, live),restrict,search(account SERP, places, recent, suggested, top search),settings(pending tags, manual tag approval),stories(reels tray, reels media, user story, media viewer, voters),tags(TagMediaFeedResponse,TagsSectionsFeedResponse,TagsStoryResponse),timeline(TimelineFeedResponseand its schemas),users(UsersInfoResponse,UsersInfoStreamResponse, user media feeds, tagged, blocked, story, search) and the challenge steps (cni,UfacFlowState.external_flow_idandenrollment_id), plusDirectThemeId(nowstring). - The inputs that took
string | numbertakestring:threadId,threadIds,userId,userIds,itemId,mediaId,broadcastId,pollId,quizId,sliderId,questionId,trackId,themeId,postId,original_media_id,leadGenFormId,location_id, theidofLocationInfo,LocationStoryand the clips-user feed,user_idof the media-impression, media-edit and posting inputs (PostingUsertag.user_id),recipientUsers,addandremoveof the besties,userandseriesof the IGTV series,musicBurninParams.asset_fbidof the video upload (it wasstring | number | bigint),coauthorsofPostingNewPostOptions,trayUserIdsofAndroidDirectInboxOptions,AndroidDirectThreadOptionsandAndroidDirectChangeNicknameJourneyInput, andthreadIdofDirectThreadSetWallpaper. A program that passes a number gets a type error. - Migration: keep ids as strings and compare them as strings (
media.user.pk === '555', not=== 555); replaceNumber(user.pk)with the text itself, passString(id)where you held a number, and read a stored list of ids asstring[]. A program that did arithmetic on an id converts at the one place it needs a number (BigInt(id)for a long one). The screens' data (media.pk,reel.pk, the Direct userpk) are strings too; a snapshot your program stored with a numeric id reads back as its text. A zero the server writes under an id key is the text'0', and'0'is truthy:assigned_admin_id,original_media_id,thread_fbid,cursor_thread_v2_idand the like carry0as a placeholder, soif (item.original_media_id)is true now; check emptiness with=== '0'. - Public functions whose result changed: the core's readers return the text of a number that sits under an id key —
parseJson,parseNdjson,jsonWithLongIdsAsText,fromJsonText(@igpapi/core) andBloks.parseJSON,Bloks.collectJsonLiterals,BloksResponse.json/jsonLiterals, also for an id of 15 digits or fewer (it was a number); the typed errors'body(IgApiResponseError,RequestError) and a screen item'srawnode do the same. - Other types that changed:
MediaClipSegment.face_effect_id(number | null→string | null),DirectThemeIdNotNumericError.themeIdandDirectThemeId(string),UfacFlowState.external_flow_id/enrollment_id, the navigation step'scni.
- The changed types: the
MediaOembedResponsedescribes what the library reads, which is nothing: every field is optional and a field the answer adds is kept. Migration: check a field forundefinedbefore using it. An answer that is not an object with these fields' types is the schema's error, as before for a non-JSON body.WebChallengeInfois the page as the server answered it (unknown), and the web challenge step'schallengeTypeisstring | undefined: a page without achallengeTypeis shown whole by the unimplemented screen. Migration:webChallengeTypeOf(challenge)reads the type.- Added:
NumericIdText(IdAsTextthat accepts only a canonical integer: digits, no sign, no leading zero),idAsJsonNumber(id)(an id written as a JSON number from its exact digits, where the app writes one; a text that is no canonical integer is written as a JSON string),IdAsText(the schema of an id field),fromJsonText(schema)(@igpapi/core) andBloks.fromRepairedJsonText(schema)decode a JSON text held in a string field with the same reader. - A telemetry id keeps every digit. The Pigeon builders (
@igpapi/pigeon-nest; id fieldsprofile_user_id,target_user_idinbarcelona_growth_integration_impression,entity_id,a_pk,ig_id,user_fbid, and the organic and compound events'media_id,a_pk,carousel_container_media_id) and the profile posts feed's view events (media_id,m_pk) write an id as a JSON number from the id's exact digits withidAsJsonNumber, neverNumber(id): a 19-digit id is no longer rounded to…123000. A telemetry payload may hold aJsonNumberLiteralwhere the app writes an id as a number, soMobileTelemetryEvent.payloadand a Pigeon event'sextraare typedJsonWithNumberLiterals/JsonObjectWithNumberLiterals(@igpapi/core), andJSON.stringify, the one writer, writes the literal as the bare digits. Migration:MobileTelemetryEventSchemais nowMobileTelemetryEventTrailSchema— the reader of a trail a release stored, whose payload is plain JSON; the live event is theMobileTelemetryEventtype. - A number in a Bloks AST argument, a dict value or a literal keeps every digit.
@igpapi/transportis pinned to 0.9.24, whose Bloks extract writes every number as its source text, and the core reads it exactly: a number of more than 15 characters (a 17- or 19-digit id) is a string with all its digits, a shorter one is a number, and a literal no double holds (1e999) is its text. Migration: a number you took fromBloksResponse.ast,.dictsor.literalsthat has more than 15 digits is now a string; read it as text (an id is text).
Changed: the Direct screens' public API
The Direct screens' re-verification against the 451 build (the tab's burst and the stored inbox head's prefetched first page, the picker's search with its thread rows, a conversation that loads only when the app does not already hold it) changed the surface a program uses.
DirectRecipientPickerScreen'ssearch(query)answers{ recipients, threads }— the people the answer offered and, where the search shows threads, the conversations — instead of the recipients array; the picker's data carries both lists. Migration:const { recipients, threads } = yield* picker.search(text); a caller that read the old array reads.recipients.DirectBroadcastMessage.bodyis optional, and a message may lay its own form out.DirectBroadcastMessage.compose(with the newDirectBroadcastFormParts) returns the whole form, and the broadcast adds nothing around it — the 451 reaction arranges its fields itself.DirectBroadcastInput.extrasadds fields after the navigation chain (the 451 text message'sai_assistant_extras); a name the form already holds is refused as a defect, never rewritten. Migration: a message type with abodykeeps it; a message that lays out every field returnscompose.DirectBroadcastanswersDirectBroadcastAnswer— the server's response plus theclient_contextthe request minted, so a caller that later names the sent message (a reaction'soriginal_message_client_context) reads it from the result.DirectThreadInput'ssend_attributionis a closed set —inbox,inbox_new_message,direct_thread_creation_omnipicker,message_button— and an entry that is none of them is a decode error, because the thread's sends behave per member. Migration: pass one of the four.DirectThreadDetailsis a sum on the thread's kind: a one-to-one thread (is_group: false) carries its peer'speer_user_idandpeer_user_fbid, a group (is_group: true) its first participant'slead_user_id. Migration: narrow onis_groupbefore reading a peer's field; a group record carrieslead_user_idinstead of the peer's two.
Changed: the Bloks pixel ratio comes from the platform
@igpapi/mobile's shared Bloks context builder no longer parses the iOS descriptor's scale= token: the pixel ratio comes from the new MobileDisplayScaleWire service (@igpapi/core/mobile, the sibling of the network wire), which each platform installs — iosDisplayScaleWire (@igpapi/core/ios) reads the descriptor, androidDisplayScaleWire (@igpapi/core/android) reads none, because the Android build's bk_client_context carries no pixel_ratio. The readers deviceDescriptorScaleOf and deviceDisplayScaleOf moved with it. Migration: import the two readers from @igpapi/core/ios (they left @igpapi/core/mobile), and provide MobileDisplayScaleWire (iosDisplayScaleWire or androidDisplayScaleWire) where you provide the platform's other services.
Fixed
- A carousel's short code is the carousel's own (#661). A carousel's configure answer lists its slides before the carousel itself, so reading
medias[0]gave the first slide's code and a program building/p/<code>/from the result linked a slide. The code is read from themediasentry whose id is the post's id now, for a post, a carousel, a Reel and a story alike; an answer without that entry, or aPENDINGpost whose entry has no code, leavescodeabsent rather than reporting a slide's code.
Requirements
- The oldest TypeScript a user may have is 7. TypeScript 5.9 support is gone; the published declarations are checked with TypeScript 7, and the READMEs' Requirements say TypeScript ≥ 7.
4.13.0 (2026-10-28)
A minor release: effect becomes a peer dependency of the published packages (see Migration), the library exports the generator of the authenticator code a headless login needs, a published post, Reel or story reports its short code, and a launch returns as soon as the feed is on screen, with the start-up tail running in the background (see Migration).
Changed: effect is a peer dependency
@igpapi/core, @igpapi/mobile, @igpapi/android, @igpapi/ios and @igpapi/pigeon-nest declare effect in peerDependencies (^4.0.6) instead of carrying their own copy as a dependency. Before, installing a package beside your own effect put two copies of Effect into one project, and the classes of one copy (tagged errors, services, layers) never matched the other's. As a peer there is one copy — yours — and the core's bundle imports it instead of embedding it. The packages are built against [email protected]; any effect that satisfies ^4.0.6 works.
- Migration: install
effectbeside the platform package:npm install @igpapi/android [email protected],pnpm add @igpapi/android [email protected]oryarn add @igpapi/android [email protected]. npm 7 and later and pnpm 8 and later install a missing peer by themselves; Yarn only warns, and the import fails untileffectis a dependency of your project. A project that already depends on[email protected]or a later 4.x changes nothing. - The installation section of the README now sets the GitHub Packages registry up for npm, pnpm and Yarn 2+ (Berry): the classic personal access token with
read:packages, the project.npmrcor.yarnrc.yml, and for pnpm the token line in the user-level~/.npmrc(pnpm ignores an environment variable in a project.npmrc's credentials). Each path is run as written against the packed packages before a release, on Node 22, the current Node 24 and Node 24.15.0. Yarn 4 must be 4.17.0 or newer (before 4.14.1, Node 24.15.0 fails to load the packages from Plug'n'Play withEBADF, and the README's Yarn 4 file setsnpmMinimalAgeGatefor the@igpapiscope, which Yarn knows from 4.17.0), and Yarn 2 and 3 have their own file withnodeLinker: node-modules.
Changed: the launch returns with the feed on screen
- A launch returns as soon as the feed is on screen; the rest of the start-up runs on in the process's background. The call still waits for that tail before the app closes, a session-fatal answer to it rejects the call with that same typed error, a challenge over it is answered by
otherwisebetween your program's actions, andcheckpoint()/ExportSnapshotwait for it before they read.
Added
totpCodeOf(secret, at?)(@igpapi/mobile, re-exported by@igpapi/androidand@igpapi/ios) returns the six-digit code an authenticator app shows for an account's base32 setup key — RFC 6238, HMAC-SHA-1, a 30-second step, Node'scryptoonly. A program with no person at the other device submits it where it logs in:codeScreen.submitCode(totpCodeOf(secret)). A key that is not base32 throws aRangeError.- The short code of a published media.
share()of a post or a Reel answers{ id, status, mediaIds, code? }and a story{ mediaId, code? }:codeis the media's short code (medias[0].media_dict.code) when Instagram's configure answer carries it, and the field is absent otherwise — an answer with a missingmedias, anullin its place, anullmedia_dictor anullcodenever fails a publication that has already happened.
Migration
MobileProcessBackground(@igpapi/core/mobile) changed shape:startTimer,armedTimersandrestoreTimersare gone (the process timers are Android's own, internal), and the service is now{ handOver, settled, kill }(MobileProcessBackgroundShape); the typesMobileProcessBackgroundState,MobileProcessTimerandMobileProcessTimerStartare removed from@igpapi/core/mobile(the timers they described are Android's own, internal). Migration: a program that waited for the process's background work usesawaitMobileProcessBackground(orsettled), which fails with the session-fatal error the background met; code that armed or read timers through the service has no replacement — the timers belong to the platform's process.
Documentation
The Quick Start and the two-factor README run the whole headless path with totpCodeOf (a bounded approval wait, "Try another way", the authentication app), and the Quick Start saves session.json the moment the login reaches the feed, before any action. The root README's Posting section links the modules' matrices instead of repeating them.
4.12.0 (2026-10-28)
A minor release: one fix, and it widens public types (see Migration).
Fixed
- A signed-in launch, a resume and a login's continuation no longer reject when the home timeline carries an item kind the library does not model. On 4.11.0 they rejected with
ScreenDataError { screen: 'FeedTimeline' }as soon as the livefeed/timelineanswer's firstfeed_itemselement was a kind outside the five the library knew (media_or_ad,explore_story,end_of_feed_demarcator,threads_in_feed_unit,clips_netego). The failed feed enter stoppedmainfrom running and dropped the rest of the launch's requests. - A server list whose items are a tagged union is an open union. An item of a kind the library does not model is kept as an explicit unknown item and never fails the screen:
- the home timeline (and its
Followingfilter) lists it as aFeedTimelineUnknownitem (data.type === 'unknown');data.itemis aTimelineFeedUnknownItem—{ kind: 'unknown', kindKey?, raw }, wherekindKeyis the kind the server named (the item's top-level key) andrawis the whole item as the answer carried it. The item's ownrawis the server's node, as for every item; - the search field's typeahead results do the same for a result class the library does not model: a
SearchInputUnknownResultitem whosedata.itemis the same unknown item (kindKeyis the__typenamethe server named). An element that names no kind at all ({},null, a number) is an unknown item too, without akindKey. The unknown item sends nothing and has no actions. An item of a known kind whose body does not decode still fails withScreenDataError: that is a changed answer for a kind the library models.
- the home timeline (and its
- New public names:
TimelineFeedUnknownItem,TimelineFeedUnknownItemSchemaandTypeaheadUnknownItemin@igpapi/mobile;UnknownTaggedItem,UnknownTaggedItemSchema,unknownTaggedItemOf,unknownTypenamedItemOfandisUnknownTaggedItemin@igpapi/core/mobile;FeedTimelineUnknownEntryin@igpapi/android.
Migration
The types of these lists are wider now, so TypeScript code that read a member without narrowing, or switched exhaustively, stops compiling:
TypeaheadResult(@igpapi/mobile) andTypeaheadRow(@igpapi/android),TimelineFeedResponseFeedItemsItem(@igpapi/mobile) andFeedTimelineEntry(@igpapi/android) have one more member. The typeahead result has no__typenamein it (its discriminant iskind: 'unknown'); the timeline entry hastype: 'unknown'; the typeahead row has__typename: 'unknown'.- Migration: narrow before you read a member, and add a branch for the unknown one.
ts
// typeahead results (@igpapi/mobile)
if ('__typename' in result && result.__typename === 'XDTTypeaheadSearchUser') use(result.user);
else if ('kind' in result) log(result.kindKey, result.raw); // an unmodelled class
// timeline entries (a screen's data)
switch (entry.type) {
case 'post':
/* … */ break;
case 'unknown':
log(entry.item.kindKey, entry.item.raw);
break; // new
// the other cases as before
}At run time nothing changes for a caller that only read the kinds it knew: those items are the same objects as before, and the new one appears only where the answer carries a kind the library did not model.
Dependencies
effect is the stable 4.0.6 instead of 4.0.0-beta.43 (it has no runtime dependencies, which removes the toml@3 advisories), and image-size is ^2.0.3 (GHSA-w3rx-r6r6-pgpr). Programs that compose the library's services with their own Effect code use Context.Service / Context.Reference where the beta had ServiceMap.
Snapshots of earlier releases
A snapshot saved by any release from v4.0.3 on restores: each field an earlier release wrote and this one does not hold is read with the shape that release gave it and moved to where its fact lives now (the network, the battery readings, the tracking permission, the routing map and its no-valid-map pair), the navigation is left behind as process state, and a field that cannot be understood is dropped with a logged StoredSnapshotFieldDropped record instead of failing the restore. On effect 4.0.6 Schema no longer keeps unknown properties, which the restore had relied on.
Exports of @igpapi/core/mobile that changed with it (the core has no internal/public split): legacySnapshotNetworkOf(connectionType: string | undefined) takes the stored value instead of a device object; settleDeviceAdsOptOut(device, stored: boolean | undefined) takes the stored value instead of the session; settleSessionNavigation is removed (the snapshot schema leaves the navigation behind). readReleasedProcessSessionOrigin returns an Effect (it logs a dropped field) and ReleasedProcessSessionOriginSchema declares stored as unknown; both are exported from the same barrel as before. Migration: a caller of these three passes the stored value, or drops the call: MobileSnapshotSchema reads the stored fields itself.
4.11.0
A minor release: the library moves to the Instagram 451 build's wire for the parts that were still modelled on older records, adds the creation flow's screens and two codec packages, and renames the launch entries' program option to main.
Breaking changes and migration
programis nowmain. The callback option ofAndroidIgpapi.install,launch,resumeSilently,resumeFromBackgroundandresumeByReentering, and ofIosIgpapi.installandlaunch, is namedmain. A caller that still passesprogramfails at run time withTypeError: main is not a function. The types follow:AndroidIgpapiProgramisAndroidIgpapiMainandIosIgpapiProgramisIosIgpapiMain. Migration: rename the option (and the type names, if you import them).mainnever receives a challenge. Its parameter no longer includesChallengeOutcome, and a screen action insidemainno longer fails withIgCheckpointError. A challenge is answered by the newotherwiseoption: build one record withdefineOtherwise({ … })(each key a screen of the platform, each handler typed by that screen's own handle) and pass it toinstall,launchand everyresume…. After a branch the same effect runs again andmaingoes on; a screen no branch covers rejects the call with the new typedUnexpectedScreenError(expected,found). Migration: code that caughtIgCheckpointErrorinsidemainand calledOpenChallengemoves into anotherwisebranch.OpenChallengestays for a program that composesAndroidAppitself.IosIgpapihas the rename alone: iOS has no screens, so nootherwise.- Publishing a photo, a carousel, a video or a Reel is done through the screens; the one-call posting bricks are removed. Removed from the public API:
posting.send.photo(TimelinePhotoPost),posting.send.album(AlbumPost,AlbumPhotoUpload,AlbumVideoUpload),posting.send.clip(ClipPostis now internal),ig.upload.photoandig.upload.video(UploadPhoto,UploadVideoare now internal);UploadPhotoInput.wireandUploadVideoWireInputare gone. The Direct disappearing-photo uploaders,MediaConfigure, the segmented upload bricks and the story bricks are unchanged. Migration: publish through the screens:FeedTimelineScreen.openCreation(mode)→ pick → edit →share. This needs a launched process (AndroidIgpapi.launch). FeedTimelineScreen.openCreation()takes the mode.openCreation(mode)with'post' | 'story' | 'reel'answers exactly that mode's first screen, never a union. A mode change out of STORY or REEL that the app does not offer fails with the typedPostingCreationModeNotRecordedError.PostingPhotoInputandPostingVideoInputlosetakenAtandfolder. The device's media store answers them beside the picked file, through the gallery's second argument (pickPhoto(photo, gallery?),pickVideo(video, gallery?),PostingGalleryEntry,pickMedia).- The uploads send the encoded file, not the bytes you pass. A photo goes up as the WebP the app writes, a video as the H.264 MP4 the app writes; a caller of the screens changes nothing.
PostingMusicSearchResultis a union with akinddiscriminant ('track'|'originalAudio'), andPostingMusicProductis a sum of two flows (kind: 'story-sticker'andkind: 'reel').ClipMusicOptionslosesassetId,clusterId,songName,artistNameandsessionId. All are derived fromtrack. Migration: passtrack(aReelSongTrack) and the two volumes.jsonBigStringis nowjsonWithLongIdsAsText. It returnsunknownand no longer carries.stringify. Migration: rename the import and read the value through the library's schemas.AndroidNetworkSessionStateis one session with its counters:{ kind: 'none' } | { kind: 'session', nid, privateId?, nc, fc, bc }; the'inherited'and'own'kinds are gone.ScreenNotOnTopError.reason:coveredonly while the handle's frame is still held by a kept stack; a closed, re-entered or replaced frame isnavigation-moved. Migration: code that treated a closed handle ascoveredswitches onnavigation-moved.ScreenBeforeLeave.viais required and has the typeScreenLeaveVia: everybeforeLeavedeclaration states its cause.- Checkpoints and snapshots: checkpoint format 4 ships in this release, and a checkpoint is pinned to the exact build that wrote it, so a checkpoint written by an earlier release is refused. Migration: take a fresh snapshot or checkpoint after upgrading.
Added
- Media actions on the 451 build: likes, unlikes, reposts and comments send the 451 build's wire through the screens that show them. The home timeline's posts like and unlike one child of a carousel (
likeCarouselChild(index),unlikeCarouselChild(index); a post with no such child fails withFeedCarouselChildNotFoundError). The home tab'sFollowingfilter is its own screen,FeedTimelineFollowingScreen, opened withopenFollowing(). - Profile: the viewer's own profile (
MyProfileScreen), the profile editor (openEditProfile()with the bio, username and links screens), the profile's Reels tab (openReelsTab()), the Message button (openDirectThread()) andscrollGrid(). - Search, Explore and Direct: the Explore tab refreshes its popular keywords; the typeahead accepts the keywords it overfetches and answers a repeated text from the process's memory; the Direct picker's Group chat row (
startGroupChat(),createGroup(rows)). - Reels and the catalog song: the Reels tab loads its friends lane; a Reel published with a catalog song downloads the song the way the app's player does, mixes it with the chosen volumes and uploads the mix (
MixReelAudio,ReelSongDownloadError,ReelSongTrack). - The creation flow: a photo, a carousel (photos and videos mixed), a video post, a Reel (with or without a catalog song) and a story (photo or video, with or without music) are published by walking the screens the app shows, from the feed's create button or the Reels tab's plus: the gallery, the editors, the share screens, the camera and its galleries, the music browser and its search.
- Two published codec packages:
@igpapi/ffmpeg-audio(FFmpeg as WebAssembly; LGPL-2.1-or-later) and@igpapi/media-codec(decodeJpeg,decodePng,encodeWebp,pdqHashOf,encodeAvc,posterJpegOf; the posting helpersEncodePhotoForUploadandEncodeVideoForUploadare in@igpapi/mobile/posting). - Session, device and process state: the session holds the viewer's own Facebook id (
fbidV2); the Android network session mints at the app's own events (foreground, network change, background); the device keeps its MobileConfig values (ReadMobileConfig,KeepMobileConfigAnswer);x-ig-salt-idsis rendered from the running user flows (MobileUserFlows,StartUserFlow,EndUserFlow). - Core: the screen in focus acts — an action, edge,
backor trail runs only on the top frame and a covered screen fails withScreenNotOnTopError; one JSON reader keeps a long number literal as its exact text; each request is rendered on the connection protocol (HTTP/2 or HTTP/3) the app uses for it. - Launch and resume:
mainandotherwise(see the breaking changes above).
Fixed
- Core obfuscation is deterministic and compiles valid source; the license check requires the transport's claims.
- The repost note's form is the presented build's, and it sends the app's friendly name.
- A login Bloks parse failure carries the answer's excerpt (
LoginBloksParseError.bodyExcerpt). - Pigeon telemetry is compressed synchronously; a slot's resolution ends every flow of its triggers; the challenge screens' published declarations name no stripped member.
Dependencies
@igpapi/transport is pinned to 0.9.22.