Use when publishing images to Instagram via the Graph API from a Creator or Business account. Triggers when the user wants to automate Instagram posting, pus...
---
name: instagram-publish
description: Use when publishing images to Instagram via the Graph API from a Creator or Business account. Triggers when the user wants to automate Instagram posting, push an image with caption to their account, set up Instagram API access for the first time, or troubleshoot Instagram Graph API errors like "Media download has failed" or "Unsupported post request". Assumes one-time Meta App setup is complete and an Access Token is available.
---
# 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)
```bash
# 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
↓
Creator
```
## Step 2 — Create a Meta Developer account
Visit <https://developers.facebook.com/> and sign in with your Facebook account, then:
```
Get Started
↓
Verify Email
↓
Accept Terms
```
## Step 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 token
```
The 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`
```bash
cp .env.example .env
```
`.env` contents:
```
ACCESS_TOKEN=IGAAxxxxxxxxxxxxxxxxxxxx
IG_USER_ID=2700xxxxxxxx # optional — auto-detected if missing
```
The script reads `.env` from the skill directory. If you run it from elsewhere, pass `--env /path/to/.env`.
## Publishing an image
```bash
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 |
| `--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/xxxxxxxxx
```
---
# Common errors
## "Media download has failed"
```json
{"error": {"message": "Media download has failed"}}
```
Meta's servers cannot reach your `image_url`. Fix:
- Host the image somewhere public: Alibaba Cloud OSS, 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
## "Unsupported post request"
```json
{"error": {"message": "Unsupported post request"}}
```
Almost always a token-permission issue. Fix:
- Back to **Use cases → Customize** in the App dashboard
- Confirm `instagram_content_publish` and `instagram_basic` are granted
- Re-generate the token and replace `ACCESS_TOKEN` in `.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
```
don't have the plugin yet? install it then click "run inline in claude" again.
expanded decision points for token expiry, rate limits, account type, and image reachability; added edge cases for network timeouts, caption truncation, and 429 rate limit responses; formalized input contract with env var names and auto-detection behavior; specified http timeouts (10s validation, 60s image download, 30s publish); clarified output format and success criteria for shell integration.
publish images to instagram through meta's graph api. the skill handles token validation, container creation, and publishing in one command. assumes meta app setup is complete and an access token is available.
use this skill when you need to post a single image to instagram programmatically. trigger it when the user wants to automate instagram posting, push an image with caption to their account, set up instagram graph api access for the first time, or troubleshoot errors like "media download has failed" or "unsupported post request". the skill bundles three graph api calls (token validation, container creation, publish) into a single command. does not handle reels, stories, carousels, or local files.
meta graph api access:
ACCESS_TOKEN: long-lived instagram graph api token (starts with IGAA). required. store in .env file as ACCESS_TOKEN=IGAAxxxx.... regenerate in meta app dashboard if expired or permissions change.IG_USER_ID: instagram user id (10-13 digits). optional. auto-detected from token validation step if omitted. can be passed via .env or retrieved manually at meta app dashboard under use cases.image_url: publicly reachable url to the image file (jpg, png). must be accessible by meta's servers without authentication. required per invocation.caption: text to post with the image (supports hashtags, mentions, newlines). optional. max length 2200 characters..env file path: defaults to ./.env in script directory. can override with --env flag.external connection:
https://graph.instagram.com/. requires http outbound access. no proxy or firewall blocks on port 443.image_url publicly (no auth required). examples: aws s3, github raw, alibaba oss, cloudflare, any cdn.edge cases to handle:
/me call./me call returns no id field if token is invalid or revoked.prerequisite: instagram account must be creator or business type (not personal). meta app created and token generated. see part 1 manual setup section below for one-time browser steps.
input: .env file or env vars.
ACCESS_TOKEN from .env or $ACCESS_TOKEN env var.IG_USER_ID from .env or $IG_USER_ID env var (optional).IGAA, warn user: "token format unexpected. should start with IGAA".output: ACCESS_TOKEN, IG_USER_ID (or null if not provided).
input: ACCESS_TOKEN.
https://graph.instagram.com/v24.0/me?fields=id,username&access_token={ACCESS_TOKEN}.if request succeeds (http 200):
id and username from response json.IG_USER_ID was null, set it to the id from response.if request fails (http 401, 400, or network timeout):
output: validated IG_USER_ID (string).
input: image_url (required), caption (optional string).
image_url is empty or not provided, exit with error "image_url required".image_url does not start with http:// or https://, exit with error "image_url must be absolute http(s) url".caption length exceeds 2200 characters, truncate and warn: "caption exceeds 2200 chars, truncating".output: validated image_url, caption (string, may be empty).
input: ACCESS_TOKEN, IG_USER_ID, image_url, caption.
https://graph.instagram.com/v24.0/{IG_USER_ID}/media?image_url={image_url}&caption={caption}&access_token={ACCESS_TOKEN}.if request succeeds (http 200):
id from response json. this is the creation_id.if request returns specific error messages:
if network timeout or no response:
output: creation_id (string).
input: ACCESS_TOKEN, IG_USER_ID, creation_id, dry_run flag.
if --dry-run flag was passed:
if publish enabled:
https://graph.instagram.com/v24.0/{IG_USER_ID}/media_publish?creation_id={creation_id}&access_token={ACCESS_TOKEN}.if request succeeds (http 200):
id from response json. this is the post id.https://www.instagram.com/p/{post_id}.if request fails (http 4xx, 5xx, or timeout):
output: post_id (string), instagram post url (string).
if access token missing or invalid: exit immediately with instruction to regenerate token in meta app dashboard. do not proceed to container creation.
if image url is not publicly reachable: stop at step 4. print diagnostic: "open image_url in incognito window. if you cannot see it, meta cannot either". do not retry.
if caption exceeds 2200 characters: truncate silently and warn user in output. do not fail the publish.
if --dry-run flag is set: skip step 5 (publish). validate token and create container only. exit with success after step 4.
if rate limit hit (http 429): print "api rate limit exceeded (200 calls per hour). retry in 5 minutes". exit with backoff code. do not retry automatically.
if token has expired (http 401 on /me call): do not attempt container creation. exit immediately with instruction to regenerate token. long-lived tokens expire after ~60 days.
if account type is not creator or business: this is caught during meta app setup (step 1 manual setup). the skill does not validate account type. if errors occur during container creation that suggest personal account, print "account may be personal. switch to creator or business account at https://www.instagram.com/accounts/professional_account_settings/".
on success:
✓ token valid , user: {username} (id: {ig_user_id})
✓ container created: {creation_id}
✓ published: {post_id}
https://www.instagram.com/p/{post_id}
output is printed to stdout. shell exit code is 0.
on failure, one of:
token validation failed: {error_message}
media download failed. check that image_url is public and reachable without auth.
token permissions insufficient. regenerate token with instagram_content_publish and instagram_basic scopes.
publish failed: {error_message}. container created but post not published.
on dry-run success:
✓ token valid , user: {username} (id: {ig_user_id})
✓ container created: {creation_id}
dry-run mode: skipping publish step. token and container validated successfully.
exit code is non-zero (1 or 4) on failure. shell scripts can check $? to determine success.
the user knows the skill worked when:
https://www.instagram.com/p/ABC123XYZ/).the user knows it failed if:
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.
visit https://www.instagram.com/accounts/professional_account_settings/ and follow:
Settings
↓
Account Type
↓
Switch to Professional Account
↓
Creator
visit https://developers.facebook.com/ and sign in with your facebook account, then:
Get Started
↓
Verify Email
↓
Accept Terms
visit https://developers.facebook.com/apps/:
Create App
↓
Use cases: Manage messaging & content on Instagram
↓
Do NOT connect a Business portfolio
warning: skipping the business portfolio avoids the "insufficient developer permissions" error later.
in App settings → Basic:
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 token
the token starts with IGAA.... save it to .env (next section).
| field | example | location |
|---|---|---|
ACCESS_TOKEN |
IGAAxxxxx... |
.env |
IG_USER_ID |
2700xxxxxx |
.env (optional, auto-fetched) |
USERNAME |
xxxxx |
reference only |
warning: never commit .env to git. the token authenticates posts on your behalf.
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)
.envcp .env.example .env
.env contents:
ACCESS_TOKEN=IGAAxxxxxxxxxxxxxxxxxxxx
IG_USER_ID=2700xxxxxxxx # optional, auto-detected if missing
the script reads .env from the skill directory. if you run it from elsewhere, pass --env /path/to/.env.
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 |
--caption TEXT |
empty | instagram 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 container, skip final publish |
successful output:
✓ token valid , user: xxxxx (id: 2700xxxxxx)
✓ container created: xxxxx
✓ published: xxxxx
https://www.instagram.com/p/xxxxxxxxx
{"error": {"message": "Media download has failed"}}
meta's servers cannot reach your image_url. fix:
localhost, 127.0.0.1, and private network ips.{"error": {"message": "Unsupported post request"}}
almost always a token permission issue. fix:
instagram_content_publish and instagram_basic are granted.ACCESS_TOKEN in .env.long-lived tokens still expire (default ~60 days). if /me returns an auth error, regenerate the token in the app dashboard and update .env.
meta applies 200 api calls per hour per token. if you hit this, wait 60 minutes before retrying.
meta's servers may fail to download large images. if container creation times out, check image file size (keep under 20 mb) and test the url in a browser to confirm it is reachable.
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