Instagram Media Downloader avatar

Instagram Media Downloader

Pricing

from $9.12 / 1,000 file storeds

Go to Apify Store
Instagram Media Downloader

Instagram Media Downloader

Downloads Instagram post video/image/cover files into your run's storage and probes each one with ffprobe (duration, resolution, fps, audio, codec). Takes rows copied from ig-profile-scraper or bare post/reel URLs. Logged out, no login. A file that fails to download is never charged.

Pricing

from $9.12 / 1,000 file storeds

Rating

0.0

(0)

Developer

Adrian Voss

Adrian Voss

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Share

Give this actor Instagram post references — rows copied straight from ig-profile-scraper's dataset, or plain post/reel URLs — and it downloads each post's video, image, or video cover into your own Apify storage, hashes it, and probes it with ffprobe for duration, resolution, frame rate, audio and codec. Listing and downloading are two different jobs; this one is the download half.

Who it's for

An agent or workflow that already listed a creator's or competitor's reels with ig-profile-scraper now needs the actual files, not just the CDN links Instagram hands out — links that expire on their own schedule and vanish the moment nobody fetches them. A media-ops team archiving a set of reels for compliance or a highlight reel wants durable copies in one place, with a checksum proving what was actually saved. A data pipeline validating scraped video metadata needs real ffprobe numbers — duration, resolution, fps, codec — not whatever a third party claims those are. All three get the same thing here: post references in, files landed in your run's key-value store, with the exact bytes hashed and probed.

Why this one

  • It re-resolves, it doesn't just trust you. Instagram's CDN URLs (?efg=...&oh=...&oe=...) expire unpredictably. Give this actor a video_url you copied an hour ago and, if it's gone stale, it falls back to the post's own embed page — and then, only if that's not enough either, the full post page — for a fresh one. You never see a dead link turn into a failed row.
  • Every file is hashed while it streams. sha256 is computed as the download happens, never from a re-read after the fact, so it's a checksum of exactly what got stored, not of what a second pass happened to find on disk.
  • Real ffprobe numbers, not guesses. duration_s, width, height, fps and codec come from actually running ffprobe against the downloaded bytes — the same tool you'd run yourself, just already run for you.
  • A failed download is never charged. An expired link, a deleted post, a file over your size cap — each comes back as a normal row with error set and every probe field null. Only a row that actually landed in storage counts toward the bill.
  • The CDN fetch never touches a proxy. Media bytes are streamed directly; only the two page fetches that can involve re-resolving a stale link (the embed, and — rarely — the full post page) go through Apify Proxy, and only when a link needs resolving at all.

What you get

One row per file by default (turn off "Expand rows" in Advanced to get one row per post reference instead, with every file nested under files).

FieldTypeDescription
querytextThe shortcode you submitted, echoed back
foundbooleantrue if at least one file was attempted for this post
statustextOK on a match; NOT_FOUND, BLOCKED or BAD_FORMAT on a miss
messagetextPlain-English reason for a miss — only present when found is false
shortcodetextInstagram's own post code
fileCountnumberFiles successfully stored and billed for this post
kindtextvideo, image, or cover (a video post's still)
kvs_keytextThe key this file was stored under in the run's default key-value store
kvs_urltext (link)Full URL to fetch the file — needs your own Apify token to read it, since the store is private
bytesnumberFile size
sha256textSHA-256, computed while streaming
duration_snumberVideo length in seconds. null for a still image
width / heightnumberPixel dimensions
fpsnumberFrame rate, from ffprobe's r_frame_rate. null for a still image
has_audiobooleanWhether ffprobe found an audio stream. null for a still image
codectextffprobe's codec name — the video codec, or the still image's own codec
source_urltext (link)The URL that was actually downloaded
resolvedbooleantrue if the URL you supplied was missing or expired and this actor found a fresh one itself
downloaded_atdateWhen this file finished downloading
errortextWhy this specific file couldn't be stored. null on success — this is what fileCount excludes
scrapedAtdateWhen this row was produced

Deselect any field in the Input tab's column picker to drop it from every row — query, found, status, message and scrapedAt always stay.

Scope and limits

  • One video / one image per post, not a full carousel. A carousel post's embed page carries a slide for each item, but this build stores only one representative still per carousel (its own cover). Per-slide downloads for carousels are a possible follow-up, not something this actor does today — error/message say so plainly when a carousel is asked for "Video only".
  • Logged out, same as ig-profile-scraper. No login, no cookies, no session token, and no input field that could accept one.
  • kvs_url needs your own Apify token. The run's default key-value store is private, matching every other Apify run. Fetch kvs_url with Authorization: Bearer <YOUR_TOKEN> (or ?token=), the same way you'd read any other private store.

Pricing

  • File stored: $12 per 1,000 files
  • Megabyte downloaded: $0.005 per MB

Plus a $0.00005 start fee per run. Each event above is billed independently, only when it actually returns data — misses (found:false) are never charged.

Billing is per file: one file event for every row that actually landed in storage. A row whose download failed (an expired link nothing could re-resolve, a post that's gone, a file over your size cap) is never charged — you only pay for bytes you actually got.

On top of that flat fee, a megabyte event is charged per megabyte (rounded up) of each successfully stored file — a small cover image costs a fraction of a cent, a large video is billed for its real size, and a file that fails to download is never charged for either event.

How to use

  1. In the Apify Console. Open the actor page and click Start — the posts field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
  2. Via the API. Call it directly with a POST request — no Console needed once you have an API token:
    curl "https://api.apify.com/v2/acts/accountable_eel~ig-media-downloader/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"posts":["{\"shortcode\":\"DdG4RIxIPyf\",\"video_url\":\"https://instagram.fmga4-1.fna.fbcdn.net/o1/v/t2/f2/m86/AQPDJtVww4K_686Ed9TbciE3dp0nrA4iUd5M8OJJIZKcU-RmkzTGwtpi3CsZY9Pgqj7h7kSpk9qe7k5S4JdWZ-Z2s7pzWC2DxuqU8rc.mp4\",\"display_url\":\"https://instagram.fmga4-1.fna.fbcdn.net/v/t51.82787-15/802503979_18695767555019133_6909214034622416488_n.jpg\"}","{\"shortcode\":\"DdcjXAeFyr8\",\"display_url\":\"https://instagram.fmga4-1.fna.fbcdn.net/v/t51.82787-15/817719321_18698673607019133_2085790344713099622_n.jpg\"}"]}'
  3. On a schedule. Save this actor as an Apify Task with the input you want, then add a Schedule (hourly, daily, weekly) so it runs on its own — no server of your own required.
  1. Pipe ig-profile-scraper's post rows straight in: each row's JSON —
    {"shortcode":"...", "video_url":"...", "display_url":"..."}
    — is exactly the shape posts accepts, so no reshaping is needed between the two actors.
  2. Set "Which media to download" to Image only if you only need a video's cover (or an image post's picture) and not the video file itself — it skips the biggest part of the download.

Input

{
"posts": [
"{\"shortcode\":\"DdG4RIxIPyf\",\"video_url\":\"https://instagram.fmga4-1.fna.fbcdn.net/o1/v/t2/f2/m86/AQPDJtVww4K_686Ed9TbciE3dp0nrA4iUd5M8OJJIZKcU-RmkzTGwtpi3CsZY9Pgqj7h7kSpk9qe7k5S4JdWZ-Z2s7pzWC2DxuqU8rc.mp4\",\"display_url\":\"https://instagram.fmga4-1.fna.fbcdn.net/v/t51.82787-15/802503979_18695767555019133_6909214034622416488_n.jpg\"}",
"{\"shortcode\":\"DdcjXAeFyr8\",\"display_url\":\"https://instagram.fmga4-1.fna.fbcdn.net/v/t51.82787-15/817719321_18698673607019133_2085790344713099622_n.jpg\"}"
]
}

One per line: a JSON object copied from ig-profile-scraper (e.g. {"shortcode":"DdG4RIxIPyf","video_url":"..."}), a plain post/reel URL, or a bare shortcode. Accepted formats: {"shortcode":"DdcI4o0Prsz"}, https://www.instagram.com/reel/DdG4RIxIPyf/, DbIdlf0j6uA.

SettingWhat it does
Which media to downloadAll downloads the video plus its cover for a reel, or the image for an image post. Video only skips the cover. Image only downloads just the still
Max file size (MB)Aborts a single file once it exceeds this, so one oversized video can't blow your run's budget. 1–300, default 300

Sample output

queryfoundstatusshortcodefileCountfileskindkvs_keykvs_urlbytessha256duration_swidthheightfpshas_audiocodecsource_urlresolveddownloaded_aterrorscrapedAt
{"shortcode":"DdG4RIxIPyf","video_url":"https://instagram.fmga4-1.fna.fbcdn.net/o1/v/t2/f2/m86/AQPDJtVww4K_686Ed9TbciE3dp0nrA4iUd5M8OJJIZKcU-RmkzTGwtpi3CsZY9Pgqj7h7kSpk9qe7k5S4JdWZ-Z2s7pzWC2DxuqU8rc.mp4","display_url":"https://instagram.fmga4-1.fna.fbcdn.net/v/t51.82787-15/802503979_18695767555019133_6909214034622416488_n.jpg"}trueOK<all rows found (full list)><size (bytes)><duration (s)>1970-01-01T00:00:00.000Z

A file that couldn't be stored comes back as a row with "error" set (not "found": false — the post itself was found; the specific file wasn't) and is never charged.

Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

curl "https://api.apify.com/v2/acts/accountable_eel~ig-media-downloader/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
-X POST \
-H "Content-Type: application/json" \
-d '{"posts":["{\"shortcode\":\"DdG4RIxIPyf\",\"video_url\":\"https://instagram.fmga4-1.fna.fbcdn.net/o1/v/t2/f2/m86/AQPDJtVww4K_686Ed9TbciE3dp0nrA4iUd5M8OJJIZKcU-RmkzTGwtpi3CsZY9Pgqj7h7kSpk9qe7k5S4JdWZ-Z2s7pzWC2DxuqU8rc.mp4\",\"display_url\":\"https://instagram.fmga4-1.fna.fbcdn.net/v/t51.82787-15/802503979_18695767555019133_6909214034622416488_n.jpg\"}","{\"shortcode\":\"DdcjXAeFyr8\",\"display_url\":\"https://instagram.fmga4-1.fna.fbcdn.net/v/t51.82787-15/817719321_18698673607019133_2085790344713099622_n.jpg\"}"]}'

n8n. Add an HTTP Request node: Method POST, URL https://api.apify.com/v2/acts/accountable_eel~ig-media-downloader/run-sync-get-dataset-items?token=<YOUR_TOKEN>, Body Content Type JSON, JSON Body {"posts":["{\"shortcode\":\"DdG4RIxIPyf\",\"video_url\":\"https://instagram.fmga4-1.fna.fbcdn.net/o1/v/t2/f2/m86/AQPDJtVww4K_686Ed9TbciE3dp0nrA4iUd5M8OJJIZKcU-RmkzTGwtpi3CsZY9Pgqj7h7kSpk9qe7k5S4JdWZ-Z2s7pzWC2DxuqU8rc.mp4\",\"display_url\":\"https://instagram.fmga4-1.fna.fbcdn.net/v/t51.82787-15/802503979_18695767555019133_6909214034622416488_n.jpg\"}","{\"shortcode\":\"DdcjXAeFyr8\",\"display_url\":\"https://instagram.fmga4-1.fna.fbcdn.net/v/t51.82787-15/817719321_18698673607019133_2085790344713099622_n.jpg\"}"]} (swap in an expression from an earlier node for a real value).

Clay. Add an "HTTP API" column: Method POST, URL https://api.apify.com/v2/acts/accountable_eel~ig-media-downloader/run-sync-get-dataset-items?token=<YOUR_TOKEN>, Body {"posts":["{{post reference}}"]}, mapping the row's post reference into the posts array.

MCP. In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "Instagram Scraper API: Video & Photo Downloader" — the agent will find and run this actor.

The kvs_url on every successful row needs your own Apify API token to fetch — the store is private, the same as any other run's default key-value store. An MCP agent already holding your token can read it directly; a script needs ?token=<YOUR_TOKEN> appended to the URL.

The run also writes a summary to its OUTPUT key-value record:

{requested, returned, errors[], caps[]}
— how many posts were submitted, how many file rows came back, and what failed and why, without paging the whole dataset.

Tips

  • Feed this actor directly from ig-profile-scraper's dataset — the row shape lines up on purpose, no reshaping needed.
  • Set "Which media to download" to Video only when you already have stills from elsewhere and only need the video file — it skips a whole file per post.
  • A resolved: true row means the link you supplied was already stale and this actor found a fresh one on its own — nothing to fix on your end, just useful to know your source data ages.
  • Read kvs_url promptly. It points at this run's storage, and while Apify key-value stores don't expire quickly, treat the run's dataset as the durable record and copy files out if you need them to outlive the run's own retention.

vs. alternatives

What it costsWhat you getTrade-off
This actor (ig-media-downloader)Per file actually stored; nothing for a failed downloadDurable copies of Instagram post media in your own storage, hashed and ffprobed, with automatic re-resolution of expired CDN linksOne video/image per post, not a full carousel; logged out, so only public posts
apify/instagram-scraper (and similar listing actors)Per post listedPost metadata plus direct CDN links — video_url, display_urlThe links expire; nothing downloads or verifies them for you. That's this actor's job
Downloading the links yourselfYour time, plus retry logicThe same filesYou still have to handle expired links, hashing and probing — the parts this actor exists to do once, correctly

Data & privacy

This actor reads and downloads media from public Instagram posts that anyone can see without logging in. It does not log in, does not use cookies or session tokens, and does not reveal hidden or private content.

Downloaded files may contain images or video of real people. If you process them you are the controller of that processing and need your own lawful basis for it — this actor gives you the files, not permission to use them. Not affiliated with Instagram or Meta.

FAQ

Why is resolved sometimes true even though I supplied a URL? The URL you supplied had expired or otherwise failed, and this actor found a fresh one itself — from the post's embed page, or as a last resort its full page. You still get the file; you just didn't need to have supplied a working link in the first place.

Does it download full carousels? Not per-slide, no — only one representative still per carousel post today. See "Scope and limits" above.

Why is duration_s null on some rows? Those rows are stills (an image post, or a video's cover) — duration, fps and has_audio are only meaningful for the video file itself and are deliberately left null rather than a misleading 0.

What happens if a file is bigger than my size cap? The download is aborted once it exceeds "Max file size (MB)", and the row comes back with error set and no charge — the same as any other failed download.

Do I need a proxy? Not for the CDN download itself — it never uses one. The embed/post-page fetches (only needed when a supplied link has to be re-resolved) go through the standard proxy field if you set one; the 2026-09-20 build probe needed none at all.