Skip to content

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: 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 type ScreenAnswerAcrossPush) in @igpapi/core/mobile, and the sendAcrossPush and takeAcrossPush members of every screen's enter arguments.

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/thrift wrote a negative number for an i32 of 2^30 or more and a decoder read 16191913 back; 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 are hmac.…, at.… and the hmac_ttl token the server issued; the UFS-<uuid>-<n> counter follows the process's foreground moments (n = max(fc − 1, 0)), in the batch and in x-pigeon-session-id alike.
  • The answer. The sampling and config checksums and the hmac_ttl token an answer carries are kept in MobileDeviceState.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/thrift reader fixes. BufferReader decoded every i64 wrongly (zigzagToBigint: 0 read as -1) and clamped a read past the end of its input to the last byte; the zigzag of an i32 of 2^30 or more was read negative. A read past the end is a ThriftError; BufferWriter writes a whole number or the reader's { int, num } as an i64 instead of calling methods it never had.
  • Added: MobileDeviceState.pigeonIdentities, AnalyticsSendOutcome.answer and MobileAppLifecycleState.foregroundCount in @igpapi/core/mobile.
  • Breaking (@igpapi/pigeon-nest, for a caller that builds a PigeonContext itself): sessionId is familyId; hmacTokens is authTokens (the hmac_ttl token is hmacTtlToken); identityRequest, samplingChecksum and configChecksum are required; pigeonInit records the session and sends no empty upload (the app sends none), so pigeonSendInfraBatches begins with its first batch of events; PigeonChecksum.timestamp is the text of the i64. A platform that provides its own MobileAppLifecycle adds foregroundCount (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, pass identityRequest: false and { timestamp: '0', hash: '' } for the two checksums when the upload is not a session's own, write a checksum's timestamp as a string, and return the number of foreground moments of your process (0 when it models none) from foregroundCount.

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 pushToken option is removed (AndroidIgpapiOptions.pushToken, so AndroidApp({ pushToken }), AndroidIgpapi.install({ pushToken }) and launch({ 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 keeps pushToken for the APNs token Apple issues.
  • Breaking: AndroidPushRegister and AndroidPushRegisterInput are no longer exported from @igpapi/android. The push registration is a step of the launch that the device runs by itself; a program that called AndroidPushRegister({ 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.

  • BloksResponse carries the parsed body. json (the body after the tolerant repair; undefined when the body is not JSON) and jsonLiterals (the string literals that are JSON objects, parsed) are lazy, read once and not enumerable. A BloksResponse you build yourself must provide them (createBloksResponse and bloksResponseOf do).
  • Bloks.parseJSON reads 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.collectJsonLiterals does the same for the literals it returns.
  • Migration: a number you took from parseJSON or collectJsonLiterals that 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.pk and MediaOembedResponse.author_id are string (they were number; an id of more than 15 digits could not be a number without losing digits). The core's IdAsText is the schema of such a field. Migration: stop calling Number(pk) and compare ids as strings (pk === '555', not pk === 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 a number in about 340 response types, or string | number in the inputs and some responses; it is string everywhere 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 with IdAsText, 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 QuickPromotion qp_id / qp_nux_id, the upload settings' target_id, UFAC's external_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 of account (ChallengeStateResponse, RecoveryLookupByUsernameResponse, the login, logout, profile-picture, username and security answers), activity and news inboxes, 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.native and LoginTwoFactorRequiredResponse (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 (TimelineFeedResponse and its schemas), users (UsersInfoResponse, UsersInfoStreamResponse, user media feeds, tagged, blocked, story, search) and the challenge steps (cni, UfacFlowState.external_flow_id and enrollment_id), plus DirectThemeId (now string).
    • The inputs that took string | number take string: threadId, threadIds, userId, userIds, itemId, mediaId, broadcastId, pollId, quizId, sliderId, questionId, trackId, themeId, postId, original_media_id, leadGenFormId, location_id, the id of LocationInfo, LocationStory and the clips-user feed, user_id of the media-impression, media-edit and posting inputs (PostingUsertag.user_id), recipientUsers, add and remove of the besties, user and series of the IGTV series, musicBurninParams.asset_fbid of the video upload (it was string | number | bigint), coauthors of PostingNewPostOptions, trayUserIds of AndroidDirectInboxOptions, AndroidDirectThreadOptions and AndroidDirectChangeNicknameJourneyInput, and threadId of DirectThreadSetWallpaper. 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); replace Number(user.pk) with the text itself, pass String(id) where you held a number, and read a stored list of ids as string[]. 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 user pk) 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_id and the like carry 0 as a placeholder, so if (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) and Bloks.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's raw node do the same.
    • Other types that changed: MediaClipSegment.face_effect_id (number | null → string | null), DirectThemeIdNotNumericError.themeId and DirectThemeId (string), UfacFlowState.external_flow_id / enrollment_id, the navigation step's cni.
  • MediaOembedResponse describes what the library reads, which is nothing: every field is optional and a field the answer adds is kept. Migration: check a field for undefined before 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.
  • WebChallengeInfo is the page as the server answered it (unknown), and the web challenge step's challengeType is string | undefined: a page without a challengeType is shown whole by the unimplemented screen. Migration: webChallengeTypeOf(challenge) reads the type.
  • Added: NumericIdText (IdAsText that 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) and Bloks.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 fields profile_user_id, target_user_id in barcelona_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 with idAsJsonNumber, never Number(id): a 19-digit id is no longer rounded to …123000. A telemetry payload may hold a JsonNumberLiteral where the app writes an id as a number, so MobileTelemetryEvent.payload and a Pigeon event's extra are typed JsonWithNumberLiterals / JsonObjectWithNumberLiterals (@igpapi/core), and JSON.stringify, the one writer, writes the literal as the bare digits. Migration: MobileTelemetryEventSchema is now MobileTelemetryEventTrailSchema — the reader of a trail a release stored, whose payload is plain JSON; the live event is the MobileTelemetryEvent type.
  • A number in a Bloks AST argument, a dict value or a literal keeps every digit. @igpapi/transport is 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 from BloksResponse.ast, .dicts or .literals that 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's search(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.body is optional, and a message may lay its own form out. DirectBroadcastMessage.compose (with the new DirectBroadcastFormParts) returns the whole form, and the broadcast adds nothing around it — the 451 reaction arranges its fields itself. DirectBroadcastInput.extras adds fields after the navigation chain (the 451 text message's ai_assistant_extras); a name the form already holds is refused as a defect, never rewritten. Migration: a message type with a body keeps it; a message that lays out every field returns compose.
  • DirectBroadcast answers DirectBroadcastAnswer — the server's response plus the client_context the request minted, so a caller that later names the sent message (a reaction's original_message_client_context) reads it from the result.
  • DirectThreadInput's send_attribution is 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.
  • DirectThreadDetails is a sum on the thread's kind: a one-to-one thread (is_group: false) carries its peer's peer_user_id and peer_user_fbid, a group (is_group: true) its first participant's lead_user_id. Migration: narrow on is_group before reading a peer's field; a group record carries lead_user_id instead 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 the medias entry 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 a PENDING post whose entry has no code, leaves code absent 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 effect beside the platform package: npm install @igpapi/android [email protected], pnpm add @igpapi/android [email protected] or yarn 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 until effect is 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 .npmrc or .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 with EBADF, and the README's Yarn 4 file sets npmMinimalAgeGate for the @igpapi scope, which Yarn knows from 4.17.0), and Yarn 2 and 3 have their own file with nodeLinker: 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 otherwise between your program's actions, and checkpoint() / ExportSnapshot wait for it before they read.

Added ​

  • totpCodeOf(secret, at?) (@igpapi/mobile, re-exported by @igpapi/android and @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's crypto only. 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 a RangeError.
  • The short code of a published media. share() of a post or a Reel answers { id, status, mediaIds, code? } and a story { mediaId, code? }: code is 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 missing medias, a null in its place, a null media_dict or a null code never fails a publication that has already happened.

Migration ​

  • MobileProcessBackground (@igpapi/core/mobile) changed shape: startTimer, armedTimers and restoreTimers are gone (the process timers are Android's own, internal), and the service is now { handOver, settled, kill } (MobileProcessBackgroundShape); the types MobileProcessBackgroundState, MobileProcessTimer and MobileProcessTimerStart are 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 uses awaitMobileProcessBackground (or settled), 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 live feed/timeline answer's first feed_items element 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 stopped main from 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 Following filter) lists it as a FeedTimelineUnknown item (data.type === 'unknown'); data.item is a TimelineFeedUnknownItem — { kind: 'unknown', kindKey?, raw }, where kindKey is the kind the server named (the item's top-level key) and raw is the whole item as the answer carried it. The item's own raw is 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 SearchInputUnknownResult item whose data.item is the same unknown item (kindKey is the __typename the server named). An element that names no kind at all ({}, null, a number) is an unknown item too, without a kindKey. The unknown item sends nothing and has no actions. An item of a known kind whose body does not decode still fails with ScreenDataError: that is a changed answer for a kind the library models.
  • New public names: TimelineFeedUnknownItem, TimelineFeedUnknownItemSchema and TypeaheadUnknownItem in @igpapi/mobile; UnknownTaggedItem, UnknownTaggedItemSchema, unknownTaggedItemOf, unknownTypenamedItemOf and isUnknownTaggedItem in @igpapi/core/mobile; FeedTimelineUnknownEntry in @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) and TypeaheadRow (@igpapi/android), TimelineFeedResponseFeedItemsItem (@igpapi/mobile) and FeedTimelineEntry (@igpapi/android) have one more member. The typeahead result has no __typename in it (its discriminant is kind: 'unknown'); the timeline entry has type: '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 ​

  • program is now main. The callback option of AndroidIgpapi.install, launch, resumeSilently, resumeFromBackground and resumeByReentering, and of IosIgpapi.install and launch, is named main. A caller that still passes program fails at run time with TypeError: main is not a function. The types follow: AndroidIgpapiProgram is AndroidIgpapiMain and IosIgpapiProgram is IosIgpapiMain. Migration: rename the option (and the type names, if you import them).
  • main never receives a challenge. Its parameter no longer includes ChallengeOutcome, and a screen action inside main no longer fails with IgCheckpointError. A challenge is answered by the new otherwise option: build one record with defineOtherwise({ … }) (each key a screen of the platform, each handler typed by that screen's own handle) and pass it to install, launch and every resume…. After a branch the same effect runs again and main goes on; a screen no branch covers rejects the call with the new typed UnexpectedScreenError (expected, found). Migration: code that caught IgCheckpointError inside main and called OpenChallenge moves into an otherwise branch. OpenChallenge stays for a program that composes AndroidApp itself. IosIgpapi has the rename alone: iOS has no screens, so no otherwise.
  • 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 (ClipPost is now internal), ig.upload.photo and ig.upload.video (UploadPhoto, UploadVideo are now internal); UploadPhotoInput.wire and UploadVideoWireInput are 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 typed PostingCreationModeNotRecordedError.
  • PostingPhotoInput and PostingVideoInput lose takenAt and folder. 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.
  • PostingMusicSearchResult is a union with a kind discriminant ('track' | 'originalAudio'), and PostingMusicProduct is a sum of two flows (kind: 'story-sticker' and kind: 'reel').
  • ClipMusicOptions loses assetId, clusterId, songName, artistName and sessionId. All are derived from track. Migration: pass track (a ReelSongTrack) and the two volumes.
  • jsonBigString is now jsonWithLongIdsAsText. It returns unknown and no longer carries .stringify. Migration: rename the import and read the value through the library's schemas.
  • AndroidNetworkSessionState is one session with its counters: { kind: 'none' } | { kind: 'session', nid, privateId?, nc, fc, bc }; the 'inherited' and 'own' kinds are gone.
  • ScreenNotOnTopError.reason: covered only while the handle's frame is still held by a kept stack; a closed, re-entered or replaced frame is navigation-moved. Migration: code that treated a closed handle as covered switches on navigation-moved.
  • ScreenBeforeLeave.via is required and has the type ScreenLeaveVia: every beforeLeave declaration 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 with FeedCarouselChildNotFoundError). The home tab's Following filter is its own screen, FeedTimelineFollowingScreen, opened with openFollowing().
  • 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()) and scrollGrid().
  • 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 helpers EncodePhotoForUpload and EncodeVideoForUpload are 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-ids is rendered from the running user flows (MobileUserFlows, StartUserFlow, EndUserFlow).
  • Core: the screen in focus acts — an action, edge, back or trail runs only on the top frame and a covered screen fails with ScreenNotOnTopError; 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: main and otherwise (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.

The pages of this site are the README files of the repository. The API reference is generated from the source.