
Instagram Publish
- 1 installs
- Updated June 11, 2026
- mebusw/instagram-publish
Publishes single images with captions to Instagram through Meta's Graph API from a Creator or Business account via a bundled Python script.
About
Automates Instagram single-image posting through the Meta Graph API, with one-time manual Meta app setup then a repeatable publish script. A developer uses it to programmatically post an image and caption or troubleshoot Graph API publishing errors.
- Repeatable publish.py takes a public image URL and caption
- Covers common errors like 'Media download has failed' and public-URL requirement
Instagram Publish by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,980 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mebusw/instagram-publish --skill instagram-publishAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | June 11, 2026 |
| Repository | mebusw/instagram-publish ↗ |
What it does
Publishes single images with captions to Instagram through Meta's Graph API from a Creator or Business account via a bundled Python script.
Files
Instagram Publish
Publish images to Instagram through Meta's Graph API. Part 1 is one-time manual setup (browser clicks); Part 2 is the repeatable automated publish (this skill does it for you).
When to use
- User wants to post an image to Instagram programmatically
- User has a public image URL and wants to attach a caption and publish
- User just registered a Meta App and needs the rest of the wiring done
- User is hitting "Media download has failed" or "Unsupported post request"
When NOT to use
- Posting Reels, Stories, or Carousels (this skill handles single-image posts only)
- Local files (image must be a public URL — see error #1 below)
- Account is still a personal account (must be Creator or Business first)
Quick Start (after Part 1 setup is done)
# 1. Put credentials in .env (one time)
cp .env.example .env
# edit .env with ACCESS_TOKEN (and optionally IG_USER_ID)
# 2. Publish
python3 scripts/publish.py \
--image-url "https://example.com/photo.jpg" \
--caption "Hello from Instagram API"The script prints the new post ID on success. Done.
---
Part 1: One-time manual setup (browser only)
These steps cannot be automated — they require human logins, email verification, and Meta's UI. Do them once, save the resulting ACCESS_TOKEN, and you never need to touch Meta's dashboard again unless permissions change.
Step 1 — Switch Instagram to a Creator account
Visit <https://www.instagram.com/accounts/professional_account_settings/> and follow:
Settings
↓
Account Type
↓
Switch to Professional Account
↓
CreatorStep 2 — Create a Meta Developer account
Visit <https://developers.facebook.com/> and sign in with your Facebook account, then:
Get Started
↓
Verify Email
↓
Accept TermsStep 3 — Create an App
Visit <https://developers.facebook.com/apps/>:
Create App
↓
Use cases: Manage messaging & content on Instagram
↓
Do NOT connect a Business portfolio⚠️ Skipping the Business portfolio avoids the "insufficient developer permissions" error later.
Step 4 — Finish App settings
In App settings → Basic:
- Upload an app icon
- Fill in Privacy Policy URL and Data Deletion URL
- Pick an app category
- (Optional) In Use cases → Customize, configure account linking at <https://accountscenter.instagram.com/connected_experiences/>
- Click Publish to make the app live
Step 5 — Generate the Access Token
Use cases → Customize
↓
Left sidebar: API setup with Instagram login
↓
Add required messaging permissions
↓
Add account → authorize your Instagram account
↓
Generate token
↓
Check "I understand" → copy the tokenThe token starts with IGAA.... Save it to .env (next section).
Credentials you should have after Part 1
| Field | Example | Lives in |
|---|---|---|
ACCESS_TOKEN | IGAAxxxxx... | .env |
IG_USER_ID | 2700xxxxxx | .env (optional — script can auto-fetch) |
USERNAME | xxxxx | reference only |
⚠️ Never commit .env to git. The token is the only thing that authenticates posts on your behalf.---
Part 2: Automated publishing (the skill does this)
The three Graph API calls below are bundled in scripts/publish.py. You run one command; the script runs all three.
[1] Validate token → GET /me?fields=id,username
↓
[2] Create container → POST /{ig_user_id}/media (image_url + caption)
↓
[3] Publish → POST /{ig_user_id}/media_publish (creation_id)Setting up .env
cp .env.example .env.env contents:
ACCESS_TOKEN=IGAAxxxxxxxxxxxxxxxxxxxx
IG_USER_ID=2700xxxxxxxx # optional — auto-detected if missingThe script reads .env from the skill directory. If you run it from elsewhere, pass --env /path/to/.env.
Publishing an image with public reachable url
python3 scripts/publish.py \
--image-url "https://example.com/photo.jpg" \
--caption "Hello from Instagram API"Optional flags:
| Flag | Default | Notes |
|---|---|---|
--image-url URL | required | Must be publicly reachable by Meta's servers, if user provides a local file, you should invoke relevant skill to upload it to OSS/COS platform and get a public url before publishing |
--caption TEXT | empty | Instagram's caption, with hashtags and mentions |
--env PATH | ./.env | Path to a .env file |
--api-version | v24.0 | Graph API version |
--dry-run | off | Validate token and build the container, but skip the final publish |
Successful output:
✓ Token valid — user: xxxxx (id: 2700xxxxxx)
✓ Container created: xxxxx
✓ Published: xxxxx
https://www.instagram.com/p/xxxxxxxxxNote:
- 单图:确认图片存在且格式为 JPG/PNG。
- 多图拼接:publish.py 仅支持单图,两张图要么拼成一张,要么分两次发两条 post。IG 推荐 4:5(1080×1350)竖图,比例超过 1:2.67(高/宽)时 IG 会自动裁切, 可以用 ImageMagick 纵向拼接:
magick img1.jpg img2.jpg -append -resize 1080x /tmp/combined.jpg
# 裁切到精确 4:5
magick /tmp/combined.jpg -gravity Center -crop 1080x1350+0+0 +repage /tmp/final.jpg---
Common errors
"Media download has failed"
{"error": {"message": "Media download has failed"}}Meta's servers cannot reach your image_url. Fix:
- Host the image somewhere public: Alibaba Cloud OSS, Tencent Cloud COS, AWS S3, GitHub Raw, any CDN
- Open the URL in a fresh browser while logged out — if you can't see it, Meta can't either
- Avoid
localhost,127.0.0.1, and private network IPs
"Media ID is not available"
原因是 container 刚创建就立即 publish,解决方案是等待 30–60 秒。
"Unsupported post request"
{"error": {"message": "Unsupported post request"}}Almost always a token-permission issue. Fix:
- Back to Use cases → Customize in the App dashboard
- Confirm
instagram_content_publishandinstagram_basicare granted - Re-generate the token and replace
ACCESS_TOKENin.env
Token expired
Long-lived tokens still expire (default ~60 days). If /me returns an auth error, regenerate the token in the App dashboard and update .env.
---
Files in this skill
instagram-publish/
├── SKILL.md # this file
├── .env.example # template for ACCESS_TOKEN and IG_USER_ID
├── .gitignore # keeps .env out of git
└── scripts/
└── publish.py # the three-step publisher# Instagram Graph API credentials
# Generated in Part 1, Step 5 of the setup guide.
# NEVER commit the real .env to git.
IG_USERNAME=
ACCESS_TOKEN=
IG_USER_ID=
.env
__pycache__/
*.pyc
.DS_Store
instagram-publish
EN | 中文
Publish images to Instagram from the command line via Meta's Graph API.
- One-time setup in the Meta / Instagram web UI (5 manual steps)
- Repeatable publishing through a single Python command
- All credentials live in
.env— never committed
Requirements
- Python 3.7+
- An Instagram Creator or Business account
- A Meta Developer App with Instagram API access (see Setup)
- A publicly hosted image URL (S3, OSS, GitHub Raw, any CDN)
Install
git clone <this repo> ~/.claude/skills/instagram-publish
cd ~/.claude/skills/instagram-publish
cp .env.example .envSetup
The first-time setup is human-only — it requires logins, email verification, and clicking through Meta's UI. Follow the Part 1 section in `SKILL.md` for the full step-by-step. The short version:
1. Switch your Instagram account to Creator 2. Create a Meta Developer account at <https://developers.facebook.com/> 3. Create an App → use case "Manage messaging & content on Instagram" → do not connect a Business portfolio 4. Finish the App's basic settings (icon, privacy policy URL, data deletion URL, category) and click Publish 5. In Use cases → Customize → API setup with Instagram login, grant permissions, add your Instagram account, and click Generate token
Save the resulting token (starts with IGAA...) to .env.
Configuration
.env (copy from .env.example):
IG_USERNAME=your_handle_here # reference only — shown in success output
ACCESS_TOKEN=IGAAxxxxxxxxxxxx # required
IG_USER_ID=2700xxxxxxxxxxxxxxxxx # optional — auto-detected from /me if blank⚠️ Never commit `.env`. The token authenticates posts on your behalf. Rotate it immediately if it leaks.
Usage
# Publish an image
python3 scripts/publish.py \
--image-url "https://example.com/photo.jpg" \
--caption "Hello from the Graph API"
# Validate token and build a container, but skip the final publish
python3 scripts/publish.py \
--image-url "https://example.com/photo.jpg" \
--caption "Just checking" \
--dry-run
# Use a .env file from another location
python3 scripts/publish.py --image-url "..." --env /path/to/.env
# Pin a specific Graph API version
python3 scripts/publish.py --image-url "..." --api-version v23.0Successful output:
✓ Token valid — user: @xxxxxxxxxx (id: 2700xxxxxxxxxxxxxxx)
✓ Container created: xxxxxxxxxxxxx
✓ Published: xxxxxxxxxxxxx
https://www.instagram.com/p/xxxxxxxxxxxxxxxTroubleshooting
| Error message | Cause | Fix |
|---|---|---|
Media download has failed | Meta's servers cannot reach your image_url | Host the image on a public URL — S3, OSS, GitHub Raw, or any CDN. Open the URL in a logged-out browser to confirm. |
Unsupported post request | Token is missing publish permissions | Back in the App dashboard → Use cases → Customize → API setup with Instagram login → re-generate the token with all permissions. |
HTTP 400 / OAuthException 190 on /me | Token expired or malformed | Re-generate the token in the App dashboard and update ACCESS_TOKEN in .env. |
ACCESS_TOKEN not found | .env missing or empty | cp .env.example .env and fill in ACCESS_TOKEN. |
For deeper setup details and the full list of Graph API parameters, see `SKILL.md`.
Files
instagram-publish/
├── SKILL.md # full reference for AI agents
├── README.md # this file
├── README.zh-cn.md # 中文说明
├── .env.example # credential template
├── .gitignore # keeps .env out of git
└── scripts/
└── publish.py # the three-step publisher (stdlib only)Security notes
.gitignoreexcludes.env— keep it that way- Long-lived tokens still expire (default ~60 days); re-generate when they do
- If you ever leak a token, treat it as compromised: revoke it in the App dashboard and re-issue
instagram-publish
EN | 中文
通过 Meta Graph API,一行命令把图片发到 Instagram。
- 一次性设置:在 Meta / Instagram 网页端完成 5 步手动操作
- 重复发图:一条 Python 命令搞定,三个 API 调用全包
- 所有凭证放在
.env里,绝不提交到 git
环境要求
- Python 3.7+
- 一个 Instagram Creator(创作者) 或 Business(商家) 账号
- 一个配置好 Instagram API 访问权限的 Meta Developer App(参见设置步骤)
- 一张公网可访问的图片 URL(S3 / OSS / GitHub Raw / 任意 CDN)
安装
git clone <本仓库> ~/.claude/skills/instagram-publish
cd ~/.claude/skills/instagram-publish
cp .env.example .env设置步骤
首次设置必须人工完成 —— 涉及登录、邮箱验证、Meta 后台点击操作,无法自动化。完整流程见 `SKILL.md` 的 Part 1 部分。简要版:
1. 把 Instagram 账号切换为 Creator(创作者) 2. 在 <https://developers.facebook.com/> 创建 Meta Developer 账号 3. 创建 App → use case 选 "Manage messaging & content on Instagram" → 不要连接 Business portfolio 4. 完善 App 基本设置(图标、隐私政策 URL、数据删除 URL、类别)后点 Publish 5. 在 Use cases → Customize → API setup with Instagram login 里授予权限,添加你的 Instagram 账号,点 Generate token
把得到的 token(以 IGAA... 开头)存到 .env。
配置
.env(从 .env.example 复制):
IG_USERNAME=你的账号handle # 仅作记录,显示在成功输出里
ACCESS_TOKEN=IGAAxxxxxxxxxxxx # 必填
IG_USER_ID=2700xxxxxxxxx # 选填 —— 留空则脚本自动从 /me 拉取⚠️ 绝对不要把 `.env` 提交到 git。 这个 token 等同于你账号的发帖权限,一旦泄露立即在 Meta 后台吊销重发。
使用
# 发一张图
python3 scripts/publish.py \
--image-url "https://example.com/photo.jpg" \
--caption "Hello from the Graph API"
# 验证 token + 创建容器,但跳过最后一步发布(用来调试)
python3 scripts/publish.py \
--image-url "https://example.com/photo.jpg" \
--caption "先看看" \
--dry-run
# .env 在别的位置
python3 scripts/publish.py --image-url "..." --env /path/to/.env
# 指定 Graph API 版本
python3 scripts/publish.py --image-url "..." --api-version v23.0成功时的输出:
✓ Token valid — user: @xxxxxxx (id: 2700xxxxxxxxxx)
✓ Container created: xxxxxxxxxxxx
✓ Published: xxxxxxxxxxxx
https://www.instagram.com/p/xxxxxxxxxxxxx故障排查
| 报错 | 原因 | 解决 |
|---|---|---|
Media download has failed | Meta 服务器访问不到你的 image_url | 把图片放到公网(S3 / OSS / GitHub Raw / CDN)。在未登录的浏览器里能打开,Meta 才能访问。 |
Unsupported post request | Token 缺发帖权限 | 回到 App 后台 → Use cases → Customize → API setup with Instagram login,重新 Generate token,把权限全勾上。 |
/me 返回 HTTP 400 / OAuthException 190 | Token 过期或格式错 | 在 App 后台重新生成 token,更新 .env 里的 ACCESS_TOKEN。 |
ACCESS_TOKEN not found | .env 缺失或为空 | cp .env.example .env 并填入 ACCESS_TOKEN。 |
更详细的设置说明和 Graph API 参数,见 `SKILL.md`。
目录结构
instagram-publish/
├── SKILL.md # 完整技术参考(给 AI agent 看)
├── README.md # 英文说明
├── README.zh-cn.md # 本文件
├── .env.example # 凭证模板
├── .gitignore # 防止 .env 被提交
└── scripts/
└── publish.py # 三步发布脚本(只依赖标准库)安全提醒
.gitignore已经把.env排除 —— 不要改这条规则- 长期 token 也会过期(默认 ~60 天),过期就在后台重新生成
- token 一旦泄露,立即在 App 后台吊销并重发,不要心存侥幸
#!/usr/bin/env python3
"""
Publish an image to Instagram via the Graph API.
Three calls:
1. GET /me?fields=id,username -> validate token, fetch IG_USER_ID
2. POST /{ig_user_id}/media -> create media container
3. POST /{ig_user_id}/media_publish -> publish the container
Reads ACCESS_TOKEN (and optionally IG_USER_ID) from .env.
Requires Python 3.7+; uses only stdlib (urllib, json, argparse).
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
API_VERSION = "v24.0"
GRAPH_BASE = "https://graph.instagram.com"
# ---------------------------------------------------------------------------
# .env loading (stdlib only — no python-dotenv needed)
# ---------------------------------------------------------------------------
def load_env(path: Path) -> dict[str, str]:
"""Minimal .env parser. Skips blank lines and `#` comments."""
env: dict[str, str] = {}
if not path.exists():
return env
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip('"').strip("'")
env[key] = value
return env
# ---------------------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------------------
def http_get(url: str) -> dict:
with urllib.request.urlopen(url) as resp:
return json.loads(resp.read().decode("utf-8"))
def http_post(url: str, data: dict) -> dict:
encoded = urllib.parse.urlencode(data).encode("utf-8")
req = urllib.request.Request(url, data=encoded, method="POST")
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode("utf-8"))
def explain_error(err: urllib.error.HTTPError) -> str:
body = err.read().decode("utf-8", errors="replace")
try:
parsed = json.loads(body)
return json.dumps(parsed, indent=2, ensure_ascii=False)
except json.JSONDecodeError:
return body
# ---------------------------------------------------------------------------
# Graph API steps
# ---------------------------------------------------------------------------
def validate_token_and_get_user_id(access_token: str, cached_user_id: str | None) -> tuple[str, str]:
"""Step 1: confirm token works and return (ig_user_id, username)."""
url = f"{GRAPH_BASE}/me?fields=id,username&access_token={urllib.parse.quote(access_token)}"
try:
result = http_get(url)
except urllib.error.HTTPError as e:
raise SystemExit(
f"✗ Token validation failed (HTTP {e.code}).\n"
f" Body: {explain_error(e)}\n"
f" Likely cause: expired token or wrong value in ACCESS_TOKEN."
)
user_id = result.get("id")
username = result.get("username", "?")
if not user_id:
raise SystemExit(f"✗ /me returned no id. Full response: {result}")
if cached_user_id and cached_user_id != user_id:
print(f" note: .env IG_USER_ID={cached_user_id} but token resolves to {user_id}; using token's value")
return user_id, username
def create_container(ig_user_id: str, image_url: str, caption: str, access_token: str, api_version: str = API_VERSION) -> str:
"""Step 2: upload the image to Instagram's servers, return creation_id."""
url = f"{GRAPH_BASE}/{api_version}/{ig_user_id}/media"
try:
result = http_post(url, {
"image_url": image_url,
"caption": caption,
"access_token": access_token,
})
except urllib.error.HTTPError as e:
body = explain_error(e)
hint = ""
if "Media download has failed" in body:
hint = (
"\n Hint: Meta's servers cannot reach the image_url.\n"
" Use a public URL (S3/OSS/GitHub Raw/CDN), not localhost."
)
elif "Unsupported post request" in body:
hint = (
"\n Hint: token permissions are incomplete.\n"
" Go to App dashboard → Use cases → Customize → API setup with Instagram login,\n"
" then regenerate the token."
)
raise SystemExit(f"✗ Container creation failed (HTTP {e.code}).\n Body: {body}{hint}")
creation_id = result.get("id")
if not creation_id:
raise SystemExit(f"✗ Container creation returned no id. Full response: {result}")
return creation_id
def publish_container(ig_user_id: str, creation_id: str, access_token: str, dry_run: bool) -> str:
"""Step 3: convert container to a live post. Returns post id."""
if dry_run:
return "(dry-run, not published)"
url = f"{GRAPH_BASE}/{API_VERSION}/{ig_user_id}/media_publish"
try:
result = http_post(url, {
"creation_id": creation_id,
"access_token": access_token,
})
except urllib.error.HTTPError as e:
raise SystemExit(
f"✗ Publish failed (HTTP {e.code}).\n"
f" Body: {explain_error(e)}"
)
post_id = result.get("id")
if not post_id:
raise SystemExit(f"✗ Publish returned no id. Full response: {result}")
return post_id
def get_permalink(post_id: str, access_token: str, api_version: str = API_VERSION) -> str | None:
"""Step 4: resolve a numeric media id to its public shortcode URL."""
url = f"{GRAPH_BASE}/{api_version}/{post_id}?fields=permalink&access_token={urllib.parse.quote(access_token)}"
try:
result = http_get(url)
except urllib.error.HTTPError as e:
print(f" (warning) permalink lookup failed (HTTP {e.code}): {explain_error(e)}")
return None
return result.get("permalink")
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
global API_VERSION
parser = argparse.ArgumentParser(
description="Publish an image to Instagram via the Graph API.",
)
parser.add_argument("--image-url", required=True, help="Public URL of the image to post")
parser.add_argument("--caption", default="", help="Caption text (hashtags OK)")
parser.add_argument("--env", default=".env", help="Path to .env file (default: ./env)")
parser.add_argument("--api-version", default=API_VERSION, help=f"Graph API version (default: {API_VERSION})")
parser.add_argument("--dry-run", action="store_true", help="Validate token and build container, but skip publish")
args = parser.parse_args()
API_VERSION = args.api_version
env_path = Path(args.env)
env = load_env(env_path)
# Allow real env vars to override .env
access_token = os.environ.get("ACCESS_TOKEN") or env.get("ACCESS_TOKEN", "")
ig_user_id = os.environ.get("IG_USER_ID") or env.get("IG_USER_ID") or None
ig_username = os.environ.get("IG_USERNAME") or env.get("IG_USERNAME") or None
if not access_token:
raise SystemExit(
f"✗ ACCESS_TOKEN not found.\n"
f" Put it in {env_path} or export ACCESS_TOKEN=... in your shell."
)
if not args.image_url.startswith(("http://", "https://")):
raise SystemExit(f"✗ --image-url must be an http(s) URL, got: {args.image_url}")
# Step 1
if not ig_user_id:
print(" (IG_USER_ID not set — fetching from /me)")
resolved_id, username = validate_token_and_get_user_id(access_token, ig_user_id)
label = ig_username or username
print(f"✓ Token valid — user: @{label} (id: {resolved_id})")
# Step 2
creation_id = create_container(resolved_id, args.image_url, args.caption, access_token, api_version=API_VERSION)
print(f"✓ Container created: {creation_id}")
# Step 3
post_id = publish_container(resolved_id, creation_id, access_token, args.dry_run)
if args.dry_run:
print(f"✓ Dry-run complete — would publish creation_id={creation_id}")
else:
print(f"✓ Published: {post_id}")
# Step 4: resolve the numeric id to the real public shortcode URL
permalink = get_permalink(post_id, access_token, api_version=API_VERSION)
if permalink:
print(f" {permalink}")
else:
print(f" https://www.instagram.com/p/{post_id} (fallback — permalink lookup failed)")
if __name__ == "__main__":
main()