Save what's on your Instagram screen, the way it was posted. Stories, posts and whole profiles at original resolution, videos with sound, plus a video player, Direct multi-select and a followers check, all inside Instagram.
Works on: Chrome · Edge · Brave · Opera GX
Stories
- Two round buttons next to the open story: save this one and save all from this person. Shortcuts:
DandShift+D. Save all runs as a slow queue (new in 1.8.15): one file at a time with a random 0.5–1.2 s pause between files, a confirmation above 20 files ("Save N files? It takes about X s"), progress on the button (3/17), and a click on the button,Shift+DorEsccancels while keeping what was already saved. Any extra lookups (media/{id}/info) are capped at one every 3 s and 10 per click; items past the cap are skipped with a notice ("N could not be saved right now, try again in a moment"). The same queue serves the carousel's save all on posts and on the home feed. - Photos at the largest size Instagram has, not the size on screen (for example 1320×2346 when the viewer shows 720×1280).
- Videos with audio. By default it saves the progressive MP4 (video + sound in one file). When the original is above 720p, it fetches the separate DASH video and audio tracks and muxes them in the browser, with no re-encoding. If that fails, it falls back to the 720p file.
- Video stories at 0.5×, 1.5× or 2×: hover the speed button next to the story and pick from the vertical list. The speed sticks for the next stories (Instagram moves on when the video ends, so 2× really is twice as fast). Photo stories keep Instagram's own timer.
- Ghost mode (off by default): the "seen" request isn't sent, so viewing a story doesn't mark it as seen. A ghost badge sits in the story buttons and under our sidebar icon: lit when ghost mode is on, dimmed when it's off. Click it to turn ghost mode on or off.
Posts
- The same round buttons on an open post (from the feed or a profile, in the pop-up or on its own page): save the photo or video on screen and save the whole carousel, in order. Shortcuts:
DandShift+D. Single photos only get the first button. - They sit outside the post, to the right, and stay still while you swipe through the carousel. In a narrow window they move inside the photo's top corner.
- On the home feed too: the same buttons show up next to the post on screen, follow you as you scroll and save the photo or video you're looking at, or the whole carousel.
- Carousel counter: a small
3/9under the buttons shows which item you're on and how many the carousel has. - When there's no free space beside the post (for example a post opened from Direct, with the comments right next to it), the buttons move into the photo's top corner instead of covering the comments.
- Files land in the same place and with the same names as Download profile (
perfil/fotosorperfil/videos,YYYY-MM-DD_{code}_NN), so downloading the profile later doesn't create duplicates. You can turn the buttons off in the menu (Tools → Post buttons).
Profiles
- Large profile picture: click someone's profile picture (or hold it, if they have a story) to see it at its original size, usually 1080×1080, in a square in the middle of the screen, with a Save button. It comes from the data Instagram already loads for the profile, with no extra request. On your own profile, only holding opens it (the click still changes your photo).
- A Download profile button on any profile saves its posts (photos, videos and every carousel item) into
perfil/fotosandperfil/videos. Choose how many posts and whether you want photos, videos or both. - It scrolls the grid and keeps the responses Instagram itself loads, at a calm pace (3–6 s per page), instead of calling private endpoints.
Video player
- Laid over Instagram's own
<video>on posts and reels: play/pause, ±10 s, a seek bar, speed from 0.5× to 2×, volume and a download button. - A pause stays a pause: Instagram's feed tries to resume videos when you scroll, and the player holds them until you press play again (or tap the video yourself).
- On Reels, the bar follows the reel on screen when you move to the next one, every click on pause and the speaker counts, and the bar stays put while the mouse rests on it.
- Reels with sound. Sound is on from the start on Reels and stays on as you swipe to the next one. If you mute, it stays muted until you leave Reels. You can turn this off in the menu, under Tools.
Direct
- Pick several conversations (or All, which scrolls the list to the end so Instagram loads every page) and mark them unread/read, pin, mute or delete in one go.
- It works one conversation at a time through Instagram's own menus, with a single confirmation before deleting.
Followers and following
- Who doesn't follow you back, who you don't follow back, mutuals, and, from the second check on, who unfollowed you and new followers. Search, hide verified, sort, ignore list and CSV export.
- Unfollow or ignore in bulk: check accounts one by one or Select all, then Unfollow (N) or Ignore (N), with a single confirmation. Unfollowing goes through Instagram's own function, slowly on purpose: 8–16 s between accounts, a 3–5 min pause every 10 and at most 150 a day. It stops on any block or after 3 failures in a row.
- Safe scanning: only Instagram's
friendshipslists, 1.5–3 s per page with a pause every 15 pages, a full stop on any block or checkpoint, and an incomplete list is never saved. Results stay on your computer. - Kept per account: the scan, the ignore list and the daily unfollow limit are stored under each account's id, so switching accounts never mixes them up.
Sidebar buttons (new in 1.9.0)
- Below the ghost badge in Instagram's left sidebar: Followers and following (opens the followers tool) and Switch accounts (your current profile photo with two small arrows). Same round style as the ghost badge, with a tooltip on hover.
- Switch accounts opens Instagram's own dialog, the same way you would (More → Switch accounts), without the More menu flashing on screen. The account list and the switch itself stay with Instagram: the extension never reads Instagram's saved logins and keeps no list of your accounts. The page reloads when you switch, as it does on Instagram.
- While a long job is running (Save all, profile download, Direct, followers check or unfollow), it shows Finish or cancel what is running before switching and doesn't open anything.
- Without room in the sidebar they move to the bottom-right corner; without a sidebar (narrow window) they join the floating button's stack. They hide under Instagram's menus and in the story viewer. Turn both off in the menu → Tools → Sidebar buttons.
Everywhere
- Settings inside Instagram, not in a popup: a story-ring icon in Instagram's left sidebar (or a gear in the story viewer, or
Alt+S, or the extension icon) opens the menu. - Tidy file names:
Downloads/Instagram Stories/{user}/stories/{user}_{YYYY-MM-DD_HH-MM-SS}_{id}.jpg|mp4, using the date it was posted. Folder and per-profile subfolder are configurable. - Report a bug and Check for updates in the menu footer. Report a bug opens a GitHub issue already filled with the version and a short diagnostic (no username, no content). Check for updates reads this repo's latest release right away; the extension also checks by itself when you open Instagram, when you come back to the tab (at most every 10 minutes) and every 30 minutes.
- Update notice on the page itself: as soon as a new version is out, a compact New version vX.Y.Z card shows up by itself in the middle of the Instagram page you're on, without reloading, with what changed and an Update button that does the right thing for your browser (downloads the zip). ✕ or
Esconly snooze it: it comes back after an hour or the next time you open Instagram, until you update. - Urgent updates: when a release is marked URGENTE (a serious bug), the card can't be dismissed and the extension pauses until you update: downloads are refused with a notice, and long jobs (profile download, Direct, followers, unfollow) wait where they are and carry on by themselves after the update.
- First-run tutorial and what's new: a short tutorial the first time (Skip / Got it; reopen it with ? in the menu), and after each update a What's new card, once per version. Both also show up in the middle of the screen, never block anything and close with ✕ or
Esc. - Auto / light / dark theme (auto follows Instagram).
Download the files from the latest release, then follow the steps for your browser. After installing, reload instagram.com (tabs that were already open don't get the script until you reload). If you have other story downloaders installed, turn them off: their buttons sit on top of these.
- Download salva-stories-vX.Y.Z.zip and extract it into a folder you'll keep (for example
Documents\Salva Stories). - Open the extensions page:
chrome://extensions,edge://extensions,brave://extensionsoropera://extensions. - Turn on Developer mode, click Load unpacked and pick the folder.
- To update: extract the new version over the same folder, or use the extension's Update button (menu footer), which downloads the zip for you. The extension notices the new files within a minute and reloads itself and the Instagram tabs.
Opera GX: if the extension disappears after you close the browser, just load the folder again (a known Opera issue with unpacked extensions).
| Where | What to do |
|---|---|
| Story | Click the top button or press D to save it; the second button or Shift+D saves every story from that profile. Hover the speed button (1×) to watch a video story at 0.5×, 1.5× or 2×. |
| Post | Open it, or just scroll the home feed: the top button next to the post (or D) saves the photo or video on screen; the second one (or Shift+D) saves the whole carousel. |
| Profile | Click Download profile, pick how many posts and the types, then Download. Keep the tab open until it finishes. Click the profile picture (hold it if there's a story) to see it full size and save it. |
| Post or reel with video | Hover the video to show the player; the last button downloads it. |
| Direct | Click the select button, check conversations (or All) and pick an action in the bottom bar. |
| Followers | Click the two-people button in the sidebar (or the menu → Tools → Followers & following) → scan. Run it again later to see who unfollowed you. |
| Switch accounts | Click your photo with the arrows in the sidebar: Instagram's Switch accounts dialog opens. |
| Settings | The story-ring icon in Instagram's sidebar, the gear in stories, Alt+S or the extension icon. ? reopens the tutorial. |
Everything comes in Portuguese, English and Spanish: menu, footer, tutorial, What's new, notices, the install guide and the log that goes with Report a bug. The first time, it follows your browser's language (any other language falls back to English). Switch any time with PT · EN · ES in the menu footer (also on the tutorial and notice cards, and the flags under Appearance); the choice is saved.
- Desktop browsers on instagram.com only (Chrome, Edge, Brave and Opera GX). No mobile app.
- It depends on how Instagram's web app works today. When Instagram changes its responses or layout, a feature can stop until the extension is updated (see When Instagram changes something).
- Updates need one manual step. An extension loaded from a folder can't replace its own files, so you extract the new zip yourself (the Update button downloads it for you).
- Bulk unfollow is slow by design (at most 150 a day). Instagram may still action-block an account that unfollows a lot; use it at your own risk.
- "Save all" only covers the stories the viewer already loaded for that profile.
- Private profiles can only be downloaded or checked if you follow them.
- Big accounts are slow on purpose: profile downloads and followers checks keep a human-like pace, and the followers check stops on the first block or checkpoint.
- Direct actions need Instagram in Portuguese, English or Spanish, because they click Instagram's own menu items by their text. The Switch accounts button also finds Instagram's menu item by its text (Portuguese, English, Spanish, French, German or Italian), and needs the sidebar: in a narrow window, where Instagram has no More menu, it only shows a notice.
- The profile download folder is always named
perfil, whatever the interface language.
flowchart LR
subgraph Page["instagram.com · MAIN world"]
P["page.js<br/>fetch/XHR hook · story on screen · buttons in closed shadow DOM · ghost mode"]
M["menu.js · profile.js · player.js · inbox.js · follows.js · onboard.js<br/>settings menu · profile download · player · Direct · followers · tutorial (kit.js: shared UI)"]
end
subgraph Ext["Extension"]
B["bridge.js<br/>ISOLATED world"]
SW["sw.js<br/>service worker"]
R["remux.js<br/>DASH video + audio → one MP4"]
end
P & M -- postMessage --> B -- runtime message --> SW
SW -- chrome.downloads --> D[("Downloads")]
SW --> R --> SW
- No extra traffic in the normal case.
page.jslistens to the responses Instagram itself already downloads (the reels media GraphQL payload and inline JSON) and keeps the story items in memory. Only when an item isn't cached does it make a singlemedia/{id}/infocall. - Finding the post on screen: the post's data (every carousel item, in order) comes from the same responses Instagram already loads (the grid, the post pop-up or the inline JSON of
/p/{code}/), indexed by its shortcode. The item on screen is the image inside the carousel window whose file name matches one of the item's candidates (video: React fiber; fallback:img_indexin the URL, or on the home feed the carousel slide's owntranslateXposition). On the home feed, each post is an<article>and its shortcode comes from the post's date link. - Finding the story on screen: the biggest
<video>/<img>in the center of the viewer, matched to its media id through React fiber. When the URL already has an id, that one wins, which covers the slide animation between stories. - "Save all" uses the reel the viewer already loaded, through a slow queue. It doesn't do batch API calls, since that's what triggers Instagram's automation checks: files go one at a time with a random 0.5–1.2 s pause; the only extra lookup (
media/{id}/info, for an item the viewer listed without its media) is capped at one every 3 s and 10 per click; above 20 files it asks first; the tab counts as busy (data-ss-busy) while the queue runs so the extension doesn't reload itself in the middle; on an URGENT release the queue waits. - Minimal permissions:
downloads,storageandalarms(andpermissions.containsto warn when the browser's site access was limited); host access to Instagram's CDN (*.cdninstagram.com,*.fbcdn.net, needed to fetch DASH tracks), towww.instagram.com(to reload its tabs after an update) and toapi.github.com(to read the latest release). The service worker refuses any download URL that isn't HTTPS on the CDN hosts. Nocookies,tabsorwebRequest, and nothing about you is sent anywhere: the only outside request is that read-only release check. - One package for the four browsers: Manifest V3 with a
service_workerbackground (Chrome, Edge, Brave, Opera GX), sosw.jsuses nowindow/DOM and only promise-stylechrome.*calls. The muxed DASH video is saved through adata:URL. remux.jsis a small dependency-free fMP4 muxer: it copies themoov/trakboxes, renumberstrack_IDand interleavesmoof/mdatfragments by their decode time.
Open the console on instagram.com and run __salvaStoriesDebug(). It shows how many items are cached, the current story id and the settings:
items: 0→ the response format changed: look atharvest()andWATCHinpage.js.current: null→ the viewer layout or fiber changed: look atpkFromFiber()andcenterMedia().
Report a bug in the menu attaches the same information plus a short log. Each module also has its own helper with its state and the on-screen position of its controls: __salvaStoriesMenuDebug(), __salvaStoriesProfileDebug(), __salvaStoriesPlayerDebug(), __salvaStoriesInboxDebug(), __salvaStoriesFollowsDebug() and __salvaStoriesOnboardDebug().
End-to-end with the real extension loaded, against a simulated instagram.com and CDN. Setup is in tests/README.md; node tests/suite.mjs 3 runs everything three times in a row, and every test ends in TUDO PASSOU or FALHOU.
-
e2e-sidebar-buttons.mjs: the sidebar buttons, with placeholder accounts. Order and alignment of the four items, tooltips in PT/EN/ES, the photo of the current account (and not someone else's in a narrow window), Followers opening the tool with the saved scan, Switch accounts opening a simulated Instagram dialog through More without the menu ever being painted (also with a slow menu, a double click and an unrelated dialog that must stay visible), no read of Instagram's saved logins (watched from before any script, plus a scan of the source), the busy notice, the missing-item notice and log, Instagram's More menu over the buttons, the menu switch, a short window (corner), a narrow window (floating stack), the story viewer, a broken photo that isn't refetched, and followers data kept per account. -
e2e-save-all.mjs: the Save all queue. A 25-item reel (confirmation, Cancel downloads nothing, Save downloads 25 without duplicates, 24 measured gaps all ≥ 0.5 s and not constant, progress on the button,data-ss-busy1 while running and 0 after,Escstops it and keeps what was saved, a second click orShift+Dcancels without starting another queue), an 8-item reel (starts without asking), 15 items with no captured data (at most 10 lookups, never two within 3 s, notice for the rest), an URGENT release (queue waits and resumes; the service worker still refuses the singleD), a 10-photo carousel (same queue,_01–_10) and the confirmation in EN and ES. -
e2e.mjs, stories: photo at the largest candidate with the right name, 1080p DASH video muxed with audio (checked withffprobe), video without sound,Shift+Dsaving three, max-resolution off, ghost mode blocking only when on, and the settings menu. -
e2e-v15.mjs, everything else, in light and dark: ghost badge outside stories, a 30-post profile with carousels saved as 34 files in the right folders, the player (pause that survives the page trying to resume it, ±10 s, 2×, sound, volume, download), Direct (all 40 chats selected, 2 marked unread, 2 deleted after one confirmation), two followers checks with unfollowers and new followers, bulk ignore and unfollow, and the update notice and bug report. -
e2e-onboard.mjs(in PT, EN and ES): the tutorial on a fresh install, the ? button,Esc, What's new once after a simulated update, and a newer simulated release: the notice shows up by itself on the page, comes back after a reload, Update downloads the zip, ✕ only snoozes it, and it waits for the tutorial to close. -
e2e-update14.mjs, with the page open the whole time and a simulated GitHub: check on open, no extra check when coming back within 10 minutes, a check after that showing the card in the middle without a reload, ✕ snoozing for an hour (clock fast-forwarded) and the card back on the next visit, then an URGENTE release: no ✕,Escignored, downloads refused, long jobs paused, and everything back once it's resolved. -
e2e-post.mjs: post buttons on a carousel pop-up (photo, video, photo), swiping without the buttons moving or blinking,DandShift+Dwith the right names, a single post, a post opened on its own page, scrolling away, a narrow window, a comments panel that loads late or can't be measured (the buttons move into the photo instead of covering it) and the menu switch. -
e2e-reels.mjs: the player on a Reels-like page with human-paced clicks (press, 120 ms, release): pause and the speaker act on the reel on screen after moving to the next one, the speaker stays under the mouse when the volume slider opens, and the bar stays while the mouse rests on it. -
e2e-avatar.mjs: the large profile picture. Without a story a click opens it at 1080 without leaving the page, Save uses the right folder and name, Esc and a click outside close it; with a story (data from GraphQL) holding opens it and the story doesn't, a short click still opens the story; your own profile, a profile without the full-size data (warning) and the menu switch. -
e2e-story-speed.mjs: speed in video stories (the vertical list opens on hover, 2× applied and re-applied when the page resets it, dimmed on photo stories, kept on the next video and after a reload, the menu switch) and the ghost badge turning ghost mode on and off, in a story and on the sidebar. -
e2e-feed.mjs: buttons on the home feed (a carousel with photo, video and photo, a single photo and a reel):D,Shift+Dand a real click with the right names, the carousel counter, the player bar hiding when you swipe from the video to the next photo, moving to the next post as you scroll, hiding while scrolling, and the menu switch; plus our sidebar icon and ghost badge hiding while Instagram's "More" menu is open over them. -
e2e-profile-pill.mjs: the Download profile button stays next to Following / Message when Instagram shows the "Suggested for you" carousel under a profile you follow. -
e2e-reels-sound.mjs: sound on Reels. It's on when you enter Reels, stays on across reels, our speaker changes Instagram's own mute state, a mute holds until you leave Reels and comes back on when you return, and with the setting off nothing is touched. -
teste-pix.mjs(no browser, also in theTestworkflow): the Pix BR Code for any amount, R$ 5, R$ 10 and R$ 25 matches the reference codes character by character, decimal amounts, the CRC16 by formula and by table, and the accepted range. -
e2e-apoio.mjs: the Support ♥ box in PT, EN and ES: where it sits in the menu footer, the country step highlighted by language, focus kept inside, each QR code read back from the image and equal to its code, invalid amounts without a QR, copy buttons,Escclosing only the box, Back, and no network request. -
e2e-flick.mjs: the story buttons only ever show at their final spot while Instagram slides to the next person, and come back before the slide finishes. -
e2e-lang.mjs: first run with the browser in pt-BR, pt-PT, en-US, es-ES, es-419 and de-DE (falls back to English), then switching PT · EN · ES in the footer, tutorial, menu, What's new and the bug report log.
The screenshots on this page are the real extension (v1.7.1, and v1.8.6 for the post buttons) running on that simulated Instagram, with placeholder accounts and generated images.
Salva Stories is free. If it helps you, there's an optional Support ♥ item at the bottom of the menu: Pix for Brazil (QR code and copy-and-paste code, R$ 5 / 10 / 25 or any amount) or GitHub Sponsors for everyone else. It only opens when you click it, adds no permission, makes no network request and tracks nothing; the QR code is drawn locally.
Personal project, not affiliated with or endorsed by Instagram or Meta. Downloading content may go against Instagram's Terms of Use. Save only for personal use, don't re-post other people's content, and respect the creators.
MIT · made by André Miller Fleck (Dreds)
The Pix QR code is drawn with qrcode-generator 1.4.4 by Kazuhiko Arase (MIT), bundled unchanged in extension/src/vendor/qrcode.js.










