Guides · Updated 2026-10-25
How to Download Instagram Reels with Python (No Login)
The usual way to grab Instagram videos from Python is instaloader or yt-dlp. Both work on a laptop. Both tend to break once the script moves to a server: Instagram gives logged-out requests from datacenter IPs very little, so you start seeing login walls, 401 Unauthorized and "please wait a few minutes" responses, and the fix is usually a burner account's cookies that get flagged a week later.
This guide takes the other route: send the Reel URL to an API that does the resolving, get back direct media links, and download the file with plain requests. No Instagram account, no cookies, no proxy pool on your side.
What you need
- Python 3.8+ and
requests(pip install requests) - A soclip API key — sign in with GitHub at soclip.dev and copy it from the dashboard. New accounts get 500 free credits, and one lookup costs 2, so that is 250 Reels.
- The key in an environment variable:
export SOCLIP_API_KEY=sc_live_...
Step 1: resolve the Reel URL
POST /v1/media takes any public Instagram Reel, video post or photo post URL and returns metadata plus a list of media entries:
import os
import requests
API = "https://api.soclip.dev/v1/media"
HEADERS = {"Authorization": f"Bearer {os.environ['SOCLIP_API_KEY']}"}
def resolve(url: str) -> dict:
r = requests.post(API, headers=HEADERS, json={"url": url}, timeout=60)
body = r.json()
if not body.get("success"):
raise RuntimeError(f"{r.status_code}: {body.get('error')}")
return body["data"]data = resolve("https://www.instagram.com/reel/DcTa7nogl9z/")
print(data["author"], "-", data["title"][:60])
for m in data["medias"]:
print(m["type"], m["quality"], m["url"][:70])The response looks like this:
{
"success": true,
"credits_used": 2,
"credits_remaining": 498,
"data": {
"source": "instagram",
"title": "Caption of the post",
"author": "creator_username",
"thumbnail": "https://scontent-...cdninstagram.com/...",
"duration": 0,
"medias": [
{ "label": "HD", "type": "video", "ext": "mp4", "quality": "hd", "url": "https://api.soclip.dev/v1/dl?..." },
{ "label": "SD", "type": "video", "ext": "mp4", "quality": "sd", "url": "https://api.soclip.dev/v1/dl?..." }
]
}
}medias lists the HD video first, then SD when Instagram serves a second rendition. duration is often 0 for Instagram, so don't use it to decide anything.
Step 2: download the MP4
The media URL is a normal HTTPS link. Stream it to disk instead of loading the whole file into memory:
def download(media_url: str, path: str) -> None:
with requests.get(media_url, stream=True, timeout=120) as r:
r.raise_for_status()
with open(path, "wb") as f:
for chunk in r.iter_content(chunk_size=1 << 16):
f.write(chunk)video = next(m for m in data["medias"] if m["type"] == "video")
download(video["url"], "reel.mp4")Download soon after resolving. Instagram's CDN links are signed and expire, so store the file, not the link.
Photo posts and carousels
A photo post or carousel comes back as image entries with quality original. This saves every image plus the HD video if there is one:
import re
def save_post(url: str, out_dir: str = "downloads") -> list:
os.makedirs(out_dir, exist_ok=True)
code = re.search(r"/(?:reel|reels|p|tv)/([\w-]+)", url).group(1)
data = resolve(url)
saved = []
images = [m for m in data["medias"] if m["type"] == "image"]
video = next((m for m in data["medias"] if m["type"] == "video"), None)
for i, m in enumerate(images, 1):
path = os.path.join(out_dir, f"{code}_{i}.{m['ext']}")
download(m["url"], path)
saved.append(path)
if video:
path = os.path.join(out_dir, f"{code}.mp4")
download(video["url"], path)
saved.append(path)
return savedBatch downloads with retries
Failed lookups cost nothing — the 2 credits are refunded before the error comes back. The status code tells you whether to retry: 400 (bad URL, private or deleted post), 401 (bad key) and 402 (out of credits) will fail the same way every time; 502 and 503 are worth a retry with backoff.
import time
def resolve_with_retry(url: str, attempts: int = 3) -> dict:
for i in range(attempts):
r = requests.post(API, headers=HEADERS, json={"url": url}, timeout=60)
if r.status_code in (502, 503) and i < attempts - 1:
time.sleep(2 ** i)
continue
body = r.json()
if not body.get("success"):
raise RuntimeError(f"{r.status_code}: {body.get('error')}")
return body["data"]
with open("reels.txt") as f:
for line in f:
url = line.strip()
if not url:
continue
try:
data = resolve_with_retry(url)
video = next(m for m in data["medias"] if m["type"] == "video")
code = re.search(r"/(?:reel|reels|p|tv)/([\w-]+)", url).group(1)
download(video["url"], f"{code}.mp4")
print("ok ", url)
except Exception as e:
print("fail", url, e)Limits worth knowing
- Public content only. Private accounts and posts that need a login return an error (and are not charged).
- The same call works for TikTok, X/Twitter and Facebook URLs; only the
sourcefield changes. YouTube is not supported. - Respect the creator's rights. Downloading a Reel doesn't give you permission to republish it.
Prefer Node.js? See the Node.js version of this guide. Full request and error reference is in the docs.