Instagram Post Downloader Skill
Downloads Instagram posts at full resolution from Instagram's CDN — no screenshots, no compression. Handles single images, carousels (multi-slide posts), and Reel cover images. For carousels, produces individual slide files plus a single stitched PDF. Supports batch URLs in one run.
PREREQUISITE — Domain Allowlist
Before this skill can fetch any media, you must add Instagram's CDN domain to Claude Code's allowlist:
Settings → Capabilities → Domain allowlist → Add:
*.cdninstagram.com
Without this, all CDN fetch calls will be blocked. If you see a permission error when Claude attempts a fetch to cdninstagram.com, this is the fix.
Required Inputs
Claude will ask for these if not provided upfront:
| Input | Required | Notes |
|---|---|---|
| Instagram post URL(s) | Yes | One per line, or comma-separated. https://www.instagram.com/p/XXXX/ or https://www.instagram.com/reel/XXXX/ format |
| Output directory | No | Defaults to ./instagram-downloads/ in the current working directory |
| PDF stitch for carousels | No | Defaults to yes — produces carousel.pdf alongside individual slides |
| File naming prefix | No | Optional prefix added before slide filenames, e.g. brand_ → brand_slide_01.jpg |
Batch input example:
https://www.instagram.com/p/ABC123/
https://www.instagram.com/p/DEF456/
https://www.instagram.com/p/GHI789/
Output Structure
For each URL processed, Claude creates a folder named after the post caption (first 40 characters, sanitised — spaces become underscores, special characters stripped). If no caption is available, the folder is named after the post shortcode.
Single image post
instagram-downloads/
└── this_is_the_caption_first_40_chars/
├── image.jpg
└── metadata.txt
Carousel post
instagram-downloads/
└── carousel_caption_first_40_chars/
├── slide_01.jpg
├── slide_02.jpg
├── slide_03.jpg
├── slide_04.jpg
├── carousel.pdf ← all slides stitched in order
└── metadata.txt
Batch run (3 URLs)
instagram-downloads/
├── first_post_caption_sanitised/
│ ├── image.jpg
│ └── metadata.txt
├── second_post_carousel_caption/
│ ├── slide_01.jpg
│ ├── slide_02.jpg
│ ├── carousel.pdf
│ └── metadata.txt
└── third_post_caption_here/
├── image.jpg
└── metadata.txt
metadata.txt format
Post URL: https://www.instagram.com/p/XXXX/
Shortcode: XXXX
Type: carousel | single_image | reel
Slide count: 4 (carousel only)
Caption: [full caption text]
Username: @username
Fetched at: 2026-05-27T14:32:00Z
CDN URLs:
slide_01.jpg https://scontent.cdninstagram.com/v/...
slide_02.jpg https://scontent.cdninstagram.com/v/...
Completion summary (printed to terminal)
Instagram Post Downloader — Batch Complete
==========================================
URLs processed: 3
Posts saved: 3
Total files: 11 (9 images + 2 PDFs)
Skipped: 0
Output dir: /Users/you/project/instagram-downloads/
Results:
✓ this_is_the_caption_first_40_chars/ 1 image
✓ carousel_caption_first_40_chars/ 4 slides → carousel.pdf
✓ third_post_caption_here/ 1 image
How Claude Should Execute This Skill
Step 1 — Collect and validate inputs
- Accept the URL(s) from the user. If the user pastes a comma-separated list, split on commas. If they paste one per line, split on newlines.
- Validate each URL matches
instagram.com/p/,instagram.com/reel/, orinstagram.com/tv/. Flag malformed URLs before proceeding. - Confirm the output directory. If none provided, use
./instagram-downloads/and tell the user. - Ask about PDF stitching preference only if the user hasn't said either way. Default is yes.
Step 2 — For each URL: fetch the post page
Fetch the Instagram post page HTML:
GET https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis
Instagram frequently changes its API surface. Use this fallback chain in order:
Attempt A — JSON endpoint:
https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis
Parse the JSON response. Look for graphql.shortcode_media or data.shortcode_media.
Attempt B — Embed page (most reliable):
https://www.instagram.com/p/{shortcode}/embed/captioned/
Fetch this page's HTML and extract og:image meta tags and any window.__additionalDataLoaded or window.__StaticData JSON blobs embedded in <script> tags.
Attempt C — oEmbed endpoint:
https://api.instagram.com/oembed/?url=https://www.instagram.com/p/{shortcode}/&omitscript=true
This returns thumbnail_url — useful for single images, but only gives the first frame for carousels.
Headers to include on all requests:
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
Accept-Language: en-US,en;q=0.9
Accept: text/html,application/xhtml+xml,application/json
Step 3 — Extract CDN image URLs
From the fetched data, extract all high-resolution CDN URLs. Instagram CDN URLs follow these patterns:
https://scontent.cdninstagram.com/v/...jpg?...
https://scontent-lax3-1.cdninstagram.com/v/...jpg?...
https://instagram.fXXX1-1.fbcdn.net/v/...jpg?...
For single image posts:
- Extract the single
display_urlor the largestdisplay_resourcesentry (pick the one with the highestconfig_width).
For carousel posts:
- Look for
edge_sidecar_to_children.edges[]in the JSON. Each edge has its ownnode.display_urlandnode.display_resources[]. - Iterate all edges in order. This determines slide numbering.
- Pick the highest-resolution variant from each slide's
display_resourcesarray.
For Reels:
- The cover image is extractable the same way as a single image.
- The video file itself requires a third-party tool (see Bonus section).
If JSON extraction fails, fall back to scraping <meta property="og:image"> tags from the page HTML — this gives at least one image URL (the first slide or only image).
Step 4 — Sanitise folder name
Build the folder name from the post caption:
- Take the first 40 characters of the caption.
- Strip all characters that are not alphanumeric, spaces, or hyphens.
- Replace spaces and hyphens with underscores.
- Lowercase the result.
- Strip leading/trailing underscores.
- If the result is empty (e.g. caption was all emoji), use the post shortcode instead.
import re
def sanitise_folder_name(caption: str, shortcode: str) -> str:
truncated = caption[:40]
cleaned = re.sub(r'[^a-zA-Z0-9 \-]', '', truncated)
underscored = re.sub(r'[\s\-]+', '_', cleaned).strip('_').lower()
return underscored if underscored else shortcode
Step 5 — Create output folder structure
import os
base_dir = "./instagram-downloads"
folder_name = sanitise_folder_name(caption, shortcode)
post_dir = os.path.join(base_dir, folder_name)
os.makedirs(post_dir, exist_ok=True)
If a folder with that name already exists (e.g. running the same URL twice), append the shortcode to avoid collision: folder_name_SHORTCODE.
Step 6 — Download each image file
For each CDN URL, download the file with a streaming GET request:
import requests
def download_file(url: str, dest_path: str) -> bool:
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"Referer": "https://www.instagram.com/",
}
response = requests.get(url, headers=headers, stream=True, timeout=30)
response.raise_for_status()
with open(dest_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
return True
Name files:
- Single image:
image.jpg - Carousel slides:
slide_01.jpg,slide_02.jpg, ... (zero-padded to 2 digits, or 3 digits if >99 slides)
Detect file format from the Content-Type header or URL extension. Instagram ser