insta-boards
Local sync of Instagram Saved Collections into the filesystem. Built on top of
instagrapiwith cursor-based pagination, incremental resume and human-like throttling.
insta-boards is a small, opinionated CLI for backing up every
Saved Collection of an Instagram account into a plain, human-readable
on-disk layout. It walks the official mobile API via instagrapi,
downloads each media item (single image, video, or carousel) and keeps an
incremental state file so re-runs only pick up new items.
The default output is a flat data/raw/<slug>/ tree, where <slug> is
the transliterated collection name, and a JSON state file under
data/state/instagram_sync.json that tracks cursors, fetched items and
last-sync timestamps.
Highlights
- Full + incremental sync of every Saved Collection (state-aware, JSON-on-disk, no DB).
- Cursor-based pagination that survives restarts and partial failures.
- Human-like throttling: log-normal delays, periodic micro- and session-breaks, optional User-Agent rotation.
- Parallel carousel downloads with a bounded thread pool that still respects the pacer.
- Resilient HTTP layer: shared
requests.Sessionwithurllib3.Retryforconnect/read/statuserrors. - Idempotent re-runs: per-item metadata + content-addressed files make
--resumesafe to interrupt. - Single CLI with four subcommands —
sync,list boards,list items,download— for full sync, inspection, and per-collection download. - Pure stdlib +
uvx: no global install, runs anywhere Python 3.11+ is available.
Table of contents
- Requirements
- Installation
- Quick start
- Commands
- Configuration
- Output structure
- How it works
- Project layout
- Troubleshooting
- Roadmap
- Contributing
- License
- Acknowledgments
Requirements
- Python ≥ 3.11
uv/uvxfor the recommended one-shot install (install guide)- A working Instagram account (credentials,
sessionidcookie, or pre-savedinstagrapisettings — see Authentication) - Outbound HTTPS to
i.instagram.com,scontent-*.cdninstagram.comand the API endpoints used byinstagrapi
The project is Windows / macOS / Linux friendly; the on-disk layout uses forward slashes for cross-platform reproducibility.
Installation
From PyPI (recommended for users):
uv tool install insta-boards
insta-boards --help
Requires uv (or substitute pipx / pip if you prefer).
For development, run from a local checkout using uvx, which creates a
temporary, isolated environment and does not pollute the global Python:
# From the repository root
uvx --from . insta-boards sync --dry-run
To install the project as a long-lived tool, use uv tool:
uv tool install . # installs the `insta-boards` binary
insta-boards sync --dry-run # available globally
For development, install in editable mode with all extras:
git clone https://github.com/nimblemo/insta-boards.git
cd insta-boards
uv sync # creates .venv with all dependencies
uv run insta-boards sync --dry-run
Quick start
-
Clone and configure credentials. Create a
.envfile in the repo root (see Configuration for the full list).The most reliable way to authenticate is via a browser
sessionidcookie — log into instagram.com in your browser, open DevTools → Application → Cookies →https://www.instagram.comand copy the value of thesessionidcookie. The password-only flow often fails for accounts that were created through Facebook or have 2FA enabled.IG_USERNAME=your_login IG_PASSWORD=your_password IG_SESSIONID=your_sessionid_cookie_from_browser
-
Preview the plan (no API writes, no downloads):
uvx --from . insta-boards sync --dry-run
-
Run the full sync. State, cursors and downloaded files are written under
data/:uvx --from . insta-boards sync
-
Re-run any time — only new items are downloaded; the state file short-circuits items already known.
Commands
The package exposes a single binary — insta-boards — with four
subcommands, all defined in pyproject.toml under [project.scripts]:
| Subcommand | Purpose |
|---|---|
insta-boards sync |
Full sync of all collections (state-aware, resumable). |
insta-boards list boards |
JSONL of all collections (id, name, type, count). |
insta-boards list items |
JSONL of items in a single collection. |
insta-boards download |
Download a single collection into data/raw/<slug>/. |
The shape follows the kubectl / gh / aws / uv convention: a single binary
with verb-first subcommands, so users coming from those tools feel at
home. insta-boards --help lists every subcommand; each one also has
its own --help.
sync — full sync
# Default: every collection on the account, resumes new items only
uvx --from . insta-boards sync
# Plan only, no API/state writes
uvx --from . insta-boards sync --dry-run
# Restrict to a known set of collections
uvx --from . insta-boards sync --collection 18427410172124759
uvx --from . insta-boards sync --collection 111,222
uvx --from . insta-boards sync --collection 111 --collection 222
uvx --from . insta-boards sync --collection-file sync-collection-list.txt
# Reset progress (keep the items, drop the cursor)
uvx --from . insta-boards sync --reset
uvx --from . insta-boards sync --reset-collection 18427410172124759
# Concurrency and humanizer toggles
uvx --from . insta-boards sync --concurrency 3
uvx --from . insta-boards sync --no-humanize
# Reporting and debugging
uvx --from . insta-boards sync --report-json logs/sync-report.json
uvx --from . insta-boards sync --print-state
Format of sync-collection-list.txt (one ID per line, comma-separated is
also accepted, lines starting with # are comments).
Default location. When a
sync-collection-list.txtfile is present in the repository root,syncautomatically uses it as the collection filter — you do not need to pass--collection-fileexplicitly. If neither--collection,--collection-filenor this default file is provided, the sync walks all collections on the account.
# favorites
18143529535276037
18427410172124759,17974021309692829
17885180684721115
18040747580635287
17953107941571440
27452983781048115
2458445887976375
list boards — inspect the account
uvx --from . insta-boards list boards # walks every collection via cursor
uvx --from . insta-boards list boards --limit 50
list items — inspect one collection
# Full walk
uvx --from . insta-boards list items --collection 18427410172124759
# Cap the number of items
uvx --from . insta-boards list items --collection 18427410172124759 --limit 20
# Resume from a previously-saved cursor
uvx --from . insta-boards list items --collection 18427410172124759 \
--max-id "QV9fX0ZBS0VfQ1VSU09S" \
--output-cursor .state/items.cursor.json
download — download one collection
# Plain download of a single collection
uvx --from . insta-boards download --collection 18427410172124759
# Incremental: skip items that already have <pk>.json on disk
uvx --from . insta-boards download --collection 18427410172124759 --resume
# Resume from a saved cursor
uvx --from . insta-boards download --collection 18427410172124759 \
--max-id "QV9fX0ZBS0VfQ1VSU09S" \
--output-cursor .state/dwl.cursor.json --resume
# Override the directory name explicitly
uvx --from . insta-boards download --collection 18427410172124759 --name "Furniture"
Configuration
All configuration is read from environment variables (and optionally a
.env file in the repo root, or the current working directory). Variables
not set fall back to safe defaults.
Authentication
| Variable | Purpose | Default |
|---|---|---|
IG_USERNAME |
Login (when no saved session is present). | — |
IG_PASSWORD |
Password. | — |
IG_2FA_CODE |
One-time TOTP code (if Instagram requires 2FA). | — |
IG_SESSIONID |
sessionid cookie from web Instagram (alternative for "Log in with Facebook" accounts). |
— |
IG_PROXY |
Proxy URL (http://user:pass@host:port or socks5://host:port). |
— |
IG_SETTINGS_PATH |
Path to the instagrapi session file. |
<repo>/secrets/instagrapi.settings.json |
IG_STATE_PATH |
Path to the JSON sync state file. | <repo>/data/state/instagram_sync.json |
Login order. The CLI follows this precedence at startup:
- If
IG_SETTINGS_PATHexists — load the saved session. - If
IG_SESSIONIDis set —login_by_sessionid(), then save settings. - If
IG_USERNAME/IG_PASSWORDare set:- with
IG_2FA_CODEset —login(..., verification_code=…), - otherwise plain
login(), - then save settings.
- with
- If nothing is set — try the loaded session, otherwise a
LoginRequirederror is raised.
Network & HTTP retries
| Variable | Purpose | Default |
|---|---|---|
IG_DOWNLOAD_TIMEOUT |
Per-request HTTP timeout in seconds. | 120 |
IG_DOWNLOAD_RETRIES |
How many times to retry connect / read / status. |
5 |
IG_DOWNLOAD_BACKOFF |
Exponential backoff multiplier between retries (backoff * 2**n). |
0.5 |
IG_DOWNLOAD_DELAY |
Base pause between successful downloads (sec) — median of the human-like curve. | 1.0 |
Parallel downloads
| Variable | Purpose | Default |
|---|---|---|
IG_DOWNLOAD_CONCURRENCY |
Max simultaneous downloads inside a single item. | 1 |
IG_DOWNLOAD_POOL_REUSE |
Reuse the singleton pool across calls (1 / 0). |
1 |
Humanizer
| Variable | Purpose | Default |
|---|---|---|
IG_HUMANIZE |
Enable / disable human-like simulation (1 / 0). |
1 |
IG_HUMANIZE_SIGMA |
Sigma of the log-normal pause distribution. | 0.55 |
IG_HUMANIZE_MIN |
Minimum pause (sec). | 0.4 |
IG_HUMANIZE_MAX |
Maximum pause (sec). | 8.0 |
IG_HUMANIZE_MICRO_EVERY |
Insert a micro-break every N requests. | 12 |
IG_HUMANIZE_MICRO_MIN |
Minimum micro-break (sec). | 2.5 |
IG_HUMANIZE_MICRO_MAX |
Maximum micro-break (sec). | 6.0 |
IG_HUMANIZE_SESSION_EVERY |
Insert a session break every N requests. | 80 |
IG_HUMANIZE_SESSION_MIN |
Minimum session break (sec). | 15.0 |
IG_HUMANIZE_SESSION_MAX |
Maximum session break (sec). | 45.0 |
IG_USER_AGENT_ROTATE |
Enable / disable User-Agent rotation (1 / 0). |
0 |
Output structure
By default, the sync writes into the data/ tree next to the repository
root. Slugs are produced by transliterating the original collection name
to ASCII (e.g. Furniture → furniture, Textures: for home →
textures-for-home).
data/
├── raw/
│ └── <slug>/
│ ├── metadata.json # collection index (fetched_at, items[])
│ ├── <pk>.json # per-item metadata
│ └── <pk>_<idx>.<ext> # media files (jpg, mp4, …) per carousel index
└── state/
└── instagram_sync.json # global sync state, one record per collection
metadata.json (one per collection) is written by download
and contains the fetched_at timestamp plus a flat list of items:
{
"source": "instagram",
"fetched_at": "2026-08-27T19:42:01Z",
"items": [
{ "item_id": "3234567890123456789", "source_url": "https://www.instagram.com/p/CxYzAbC/", "taken_at": 1693152121 }
]
}
<pk>.json holds the normalised per-item record (collection id, source
url, taken_at, fetched_at, media entries with type / url / index). See
src.instagram_sync.media_entries and
src.cli.commands.list_items.normalize_media
for the exact schema.
instagram_sync.json is the global sync state used by insta-boards sync:
per-collection cursor, done flag, last_synced_at, and the dictionary
of known items. State is written after every item so a network drop
mid-run never loses progress.
How it works
┌──────────────┐ ┌────────────────┐ ┌──────────────┐
│ instagrapi │ ──▶ │ Collection- │ ──▶ │ DownloadPool │ ──▶ data/raw/<slug>/
│ Client (API) │ │ MediasPager │ │ + Pacer │
└──────────────┘ └────────────────┘ └──────────────┘
│ │ │
▼ ▼ ▼
instagrapi.Session SyncState (JSON) requests.Session + Retry
← persisted after
every successful
item
instagrapi.Client— handles login, 2FA, proxy andsessionidfallback. The session is dumped tosecrets/instagrapi.settings.jsonon success so subsequent runs skip authentication.CollectionMediasPager— a small iterator that walks the cursor API (more_available/next_max_id) for one collection, yieldingMediaobjects lazily and rememberinglast_max_idfor resume.SyncState— a single JSON file underdata/state/. The orchestrator reads the cursor and known items at start, calls the pager, and persists state after every successful item download.SessionPacer(Humanizer) — log-normal pause distribution with micro- and session-breaks. Thread-safe; consulted by every download.DownloadPool— boundedThreadPoolExecutorfor parallel carousel downloads. Every worker still goes through the pacer.
See src/humanizer.py and
src/parallel.py for the design notes.
Project layout
.
├── pyproject.toml
├── README.md
└── src/
├── __init__.py
├── client.py # auth, env loading, User-Agent rotation
├── humanizer.py # SessionPacer + HumanizerConfig
├── instagram_sync.py # state, pagers, HTTP session, raw layout
├── naming.py # slug helpers
├── pagination.py # iter_collections, CollectionMediasPager
├── parallel.py # DownloadPool (bounded thread pool)
├── paths.py # repo-root resolution
└── cli/
├── app.py # single entry point: insta-boards <subcommand>
├── _common.py # shared argparse groups, pacer/pool setup
└── commands/
├── sync.py # insta-boards sync
├── list_boards.py # insta-boards list boards
├── list_items.py # insta-boards list items
└── download.py # insta-boards download
Runtime artefacts created at first run:
secrets/
└── instagrapi.settings.json
logs/ # only if --report-json is set
.state/ # only if --output-cursor is set
data/
├── raw/<slug>/...
└── state/instagram_sync.json
Troubleshooting
"You can log in with your linked Facebook account"
The password is not accepted because the account was created via
Facebook. Set IG_SESSIONID (the sessionid cookie from web
Instagram), set a separate IG password, or rotate the IP via IG_PROXY.
2FA is requested at every run
Set IG_2FA_CODE to the current TOTP from your Authenticator app and
re-run. The session is persisted to secrets/instagrapi.settings.json,
so subsequent runs should not ask for 2FA.
ProxyAddressIsBlocked / LoginRequired
Instagram flagged the current IP. Switch to a clean residential proxy
via IG_PROXY and re-run.
A download stalls mid-collection
The run can be safely interrupted (Ctrl-C) and resumed. Re-run the
same command — the state file already short-circuits all completed
items. If the cursor is stale, use --reset or
--reset-collection <id> to start that collection from the beginning.
fake-useragent cannot download its database
Rotation falls back to a small bundled pool of current desktop
User-Agents. Set IG_USER_AGENT_ROTATE=0 to disable rotation entirely.
Tests / CI need deterministic timing
Pass --no-humanize to fall back to flat IG_DOWNLOAD_DELAY pauses.
Roadmap
- Pluggable storage backends (S3-compatible, SQLite index) behind
the
data/raw/<slug>/layout. - Configurable media-quality policy (highest vs. best per type).
- A
doctorsubcommand to validate.env, session and proxy. - Optional
watchmode: poll the Saved Collections endpoint on a schedule and run an incremental sync. - A web UI for browsing the local mirror.
Contributing
Issues and pull requests are welcome. For local development:
git clone https://github.com/nimblemo/insta-boards.git
cd insta-boards
uv sync
uv run insta-boards sync --dry-run
Please run the existing test suite (if any) and add a focused test for
new behaviour. Keep changes minimal and respect the existing module
boundaries (client, humanizer, instagram_sync, parallel,
pagination).
Releasing
Releases are fully automated via GitHub Actions (.github/workflows/):
ci.yml— runs on every push tomainand on every pull request. Matrix-builds the sdist + wheel on Python 3.11 / 3.12 / 3.13 and smoke-tests every subcommand.release.yml— runs on a published GitHub Release, or manually via the Run workflow button. Builds the wheel, smoke-tests it, and pushes it to PyPI (or TestPyPI if you pick that target) using PyPI Trusted Publishing (OIDC). No API tokens are stored in GitHub secrets.
One-time setup: register the trusted publisher on PyPI
Trusted Publishing means PyPI trusts a specific GitHub Actions workflow to upload on your behalf, so you never have to copy a token. Register it once, for both indexes:
- Create the project on PyPI (first release only):
- Go to https://pypi.org/manage/projects/publish/ and reserve the
name
insta-boards(or click publish manually the first time to claim it).
- Go to https://pypi.org/manage/projects/publish/ and reserve the
name
- Register the GitHub Actions workflow as a trusted publisher:
- On PyPI: https://pypi.org/manage/account/publishing/ → Add a
new pending publisher with:
- Owner:
nimblemo - Repository:
insta-boards - Workflow filename:
release.yml - Environment name:
pypi
- Owner:
- Repeat for TestPyPI: https://test.pypi.org/manage/account/publishing/
with environment name
testpypi.
- On PyPI: https://pypi.org/manage/account/publishing/ → Add a
new pending publisher with:
- Create the matching environments in GitHub:
- Settings → Environments → New environment → name it
pypi(andtestpypi). Optional: add protection rules (required reviewers, branch restrictions) so a stray tag cannot publish.
- Settings → Environments → New environment → name it
Cutting a release
- Bump
versioninpyproject.toml(e.g.0.1.0→0.2.0). - Commit and push:
git commit -am "release: v0.2.0" && git push. - GitHub → Releases → Draft a new release → tag
v0.2.0againstmain→ Publish release. - The
Releaseworkflow picks it up, builds, smoke-tests, and uploads the wheel + sdist to PyPI.
For a dry-run against TestPyPI first, use Actions → Release → Run workflow → target = testpypi.
Verifying a release
# published wheel — installed in a throw-away venv
uv tool install insta-boards
insta-boards --help
License
MIT — see the LICENSE file for the full text.
Acknowledgments
subzeroid/instagrapi— the unofficial Instagram API client that powers every request.fake-useragent— the optional User-Agent provider used when available.uv— the package manager used to ship a singleuvx --from .entry point.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file insta_boards-0.1.2.tar.gz.
File metadata
- Download URL: insta_boards-0.1.2.tar.gz
- Upload date:
- Size: 77.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
40fd8c8444d568efc0c846fb0e283d4fed154534ca3b15896dba6c3bd22be3cb
|
|
| MD5 |
725c16bdf820e9207a25aa51259675b8
|
|
| BLAKE2b-256 |
74bdc9b039613a09adbc2a0a0871a664985011defc6b180282b080225a0bb2d3
|
Provenance
The following attestation bundles were made for insta_boards-0.1.2.tar.gz:
Publisher:
release.yml on nimblemo/insta-boards
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
insta_boards-0.1.2.tar.gz -
Subject digest:
40fd8c8444d568efc0c846fb0e283d4fed154534ca3b15896dba6c3bd22be3cb - Sigstore transparency entry: 2634719243
- Sigstore integration time:
-
Permalink:
nimblemo/insta-boards@d5fb6922df9fe20b1f928f3e161f5561a1b25f96 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/nimblemo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d5fb6922df9fe20b1f928f3e161f5561a1b25f96 -
Trigger Event:
release
-
Statement type:
File details
Details for the file insta_boards-0.1.2-py3-none-any.whl.
File metadata
- Download URL: insta_boards-0.1.2-py3-none-any.whl
- Upload date:
- Size: 45.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e19a9c7462171a76a3a135357c991611b187407f580708ba34b9d96a3853bba2
|
|
| MD5 |
cc7c476c725538ea2870452e93aecc1e
|
|
| BLAKE2b-256 |
88c7db112b8c4e4056ab7d0a71e1f2590849339e60a28e462c0041f206252d7d
|
Provenance
The following attestation bundles were made for insta_boards-0.1.2-py3-none-any.whl:
Publisher:
release.yml on nimblemo/insta-boards
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
insta_boards-0.1.2-py3-none-any.whl -
Subject digest:
e19a9c7462171a76a3a135357c991611b187407f580708ba34b9d96a3853bba2 - Sigstore transparency entry: 2634719303
- Sigstore integration time:
-
Permalink:
nimblemo/insta-boards@d5fb6922df9fe20b1f928f3e161f5561a1b25f96 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/nimblemo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d5fb6922df9fe20b1f928f3e161f5561a1b25f96 -
Trigger Event:
release
-
Statement type: