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 saved

Batch 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 source field 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.

500 free credits on signup — 250 lookups across Instagram, TikTok, X and Facebook. Credits never expire.

Related guides