StoriesFly logo

Downloading7 min read

Download Instagram Stories with Python (No Login)

Download Instagram stories with Python and no login: a short requests script on the StoriesFly API that skips saved files, with a cron line.

StoriesFly TeamPublished
On this page
  1. How do you download Instagram stories with Python without logging in?
  2. What you need
  3. The full script
  4. Run it
  5. What the API returns
  6. How the script handles files
  7. Run it every few hours
  8. Highlights
  9. Why not Instaloader or a logged-in script?
  10. Private accounts
  11. Pricing
  12. Use it responsibly
  13. FAQ

How do you download Instagram stories with Python without logging in?

Call a story API with requests, read the stories list from the JSON answer, and save each media_url to a file. With the StoriesFly API this takes one GET request to /v1/user/stories and an API key in a header. You do not need an Instagram account, a password or cookies. The script below is about 60 lines and uses only requests plus the standard library.

StoriesFly is our API, so we use it here. The same approach works with any service that returns story media links. This article covers public accounts only.

What you need

  • Python 3.9 or newer
  • The requests package
  • A StoriesFly API key

Install the package:

pip install requests

To get a key, sign in with Telegram at /developers/dashboard. A new account gets 100 free requests, once per Telegram account, which is enough to try everything here. Keys look like sf_live_.... The API docs are at /developers/docs and the story endpoint is described at /developers/docs/user-stories. For more details, see our guide on view stories anonymously.

Keep the key out of your code. The script reads it from the STORIESFLY_KEY environment variable.

The full script

Save this as download_stories.py:

"""Download every story an Instagram account has up right now.

Usage:  STORIESFLY_KEY=sf_live_... python download_stories.py natgeo
"""
import os
import sys
from datetime import datetime, timezone
from pathlib import Path

import requests

API = "https://storiesfly.com/api/v1"
KEY = os.environ["STORIESFLY_KEY"]


def get_stories(username: str) -> list[dict]:
    r = requests.get(
        f"{API}/user/stories",
        params={"username": username},
        headers={"X-Api-Key": KEY},
        timeout=60,
    )
    if r.status_code != 200:
        # Every error is JSON: {"error": "...", "code": "..."}
        err = r.json()
        raise SystemExit(f"{r.status_code} {err['code']}: {err['error']}")
    print("Requests charged:", r.headers.get("X-Units-Charged"))
    return r.json()["stories"]


def download(url: str, path: Path) -> None:
    with requests.get(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)


def main() -> None:
    username = sys.argv[1].lstrip("@")
    stories = get_stories(username)
    if not stories:
        print(f"@{username} has no stories right now.")
        return

    folder = Path("stories") / username
    folder.mkdir(parents=True, exist_ok=True)

    for story in stories:
        ext = "mp4" if story["media_type"] == "video" else "jpg"
        when = datetime.fromtimestamp(story["taken_at"], tz=timezone.utc)
        path = folder / f"{when:%Y-%m-%d_%H-%M-%S}_{story['id']}.{ext}"
        if path.exists():
            continue  # already saved on an earlier run
        download(story["media_url"], path)
        print("saved", path)


if __name__ == "__main__":
    main()

Run it

Set the key and pass a username, with or without the @:

export STORIESFLY_KEY=sf_live_your_key_here
python download_stories.py natgeo

Output looks like this (sample values):

Requests charged: 1
saved stories/natgeo/2026-10-21_08-14-52_3412345678901234567.mp4
saved stories/natgeo/2026-10-21_09-02-10_3412345678901234999.jpg

Files land in stories/<username>/. If the account has no active stories, the script says so and exits.

What the API returns

GET /v1/user/stories?username=natgeo answers with JSON that has a stories array. Each item has these fields:

FieldMeaning
idStory id
media_typevideo or photo
media_urlLink to the file
taken_atUnix timestamp of when the story was posted
video_durationLength in seconds, for videos

A sample item, with made-up values:

{
  "id": "3412345678901234567",
  "media_type": "video",
  "media_url": "https://...",
  "taken_at": 1792570492,
  "video_duration": 12.4
}

Check the endpoint page or the OpenAPI spec at https://storiesfly.com/openapi.json for the full schema.

How the script handles files

Video or photo. media_type decides the extension: mp4 for video, jpg for everything else.

File names. Each file is named <date>_<time>_<id>.<ext>, for example 2026-10-21_08-14-52_3412345678901234567.mp4. The time comes from taken_at in UTC, so the files sort in the order the stories were posted. The id keeps two stories posted in the same second apart.

Reruns skip saved files. Because the name is built from data that never changes for a story, the same story always gets the same path. If the path exists, the script moves on without downloading. You can run it as often as you like and only new stories are fetched.

Download right away. media_url points to the Instagram CDN, and those links expire after a while. The script downloads each file in the same run that fetched the list. Do not store the links for later.

Errors. Every error answer is JSON like {"error": "...", "code": "..."}. The script prints the status and code and stops. Common codes are not_found (no such user), private_account, unauthorized (bad key), credits_exhausted and rate_limited (wait for the Retry-After header and try again).

Run it every few hours

Stories disappear 24 hours after they are posted. To keep an archive, run the script on a schedule. On Linux or macOS, crontab -e and add:

0 */4 * * * cd /home/you/story-archive && STORIESFLY_KEY=sf_live_your_key_here /usr/bin/python3 download_stories.py natgeo >> run.log 2>&1

This runs at minute 0 of every fourth hour. Each run costs one request per account, and the file downloads from the CDN are not counted, so a four-hour schedule for one account is six requests a day. With several accounts, loop over the usernames in a shell script or wrap main() in a loop. If you do, add a short pause between accounts to stay under the rate limit: 1 request per second before your first top-up, 3 per second at the Start level.

Highlights

Highlights stay on a profile and do not expire. To save them, call /v1/user/highlights?username=... to list an account's highlights, then /v1/highlight with a highlight id to get its items. The items are downloaded the same way as stories. Details are in the docs for user highlights and highlight.

Why not Instaloader or a logged-in script?

Tools that log in to Instagram, such as Instaloader, work by using a real account's session. That has trade-offs. A logged-in account that makes automated requests can be flagged, put through a checkpoint or blocked, and you need to keep the session and cookies alive. If the account matters to you, that is a risk. With a story API none of that is your concern, so your script is a plain HTTP call with a key. The cost is that it is a paid service and depends on the provider. Pick whichever trade-off suits your project.

Private accounts

Stories of private accounts cannot be downloaded this way. The API answers private_account (HTTP 403), and no private content is returned. A private account only shows its stories to approved followers, and a no-login tool has no way around that.

Pricing

Most calls count as one request. You pay as you go from a USD balance, and you can top it up with crypto, including TON and USDT. A not_found or private_account answer is charged like data. Server-side failures (upstream_unavailable, internal_error) are refunded. Current prices are on /developers/pricing.

Use it responsibly

Use this for public data and follow Instagram's terms and the law where you live. Respect the people who post the stories: do not republish their content without permission.

FAQ

Can I download Instagram stories with Python without an Instagram account?

Yes. The script calls the StoriesFly API with an API key. No Instagram or Facebook account, login or cookies are involved.

Does the script download a story twice if I run it again?

No. Files are named by posting time and story id, and the script skips any path that already exists. Reruns only fetch stories that are new.

How many requests does one run use?

One request to /v1/user/stories per account per run. The file downloads come from the Instagram CDN and are not counted. The X-Units-Charged header shows what a call cost.

Can I download stories from a private account?

No. The API returns private_account and no content. Only public accounts work.

Why do I need to run it on a schedule?

Stories are removed after 24 hours. A run every few hours saves each story before it expires.

Does it work for photo stories as well as video?

Yes. media_type is video or photo, and the script saves videos as .mp4 and photos as .jpg.