Instagram Content Publishing API: posting images, Reels and carousels
Publishing through the Instagram API takes two calls: POST /<IG_ID>/media creates a media container, and POST /<IG_ID>/media_publish publishes it. Meta downloads the media from a public URL at that moment, and images must be JPEG. Each account can publish 100 posts through the API in a rolling 24 hours; a carousel counts as one.
Published: Updated:
How does publishing through the Instagram API work?
In the standard flow you don't send the file in the request. You create a container that points to the media on a public URL, and Meta fetches and processes it. When the container's status_code is FINISHED you publish it with media_publish and receive the Instagram media ID. Videos take time to process, so their status has to be polled. Large videos can also use a resumable upload session.
The flow is the same in both setups. Instagram Login calls graph.instagram.com with an Instagram User token; Facebook Login calls graph.facebook.com with a Page access token and the Instagram account ID read from the Page.
Only professional accounts can publish. Shopping tags and filters are not supported through the API.
Permissions and tokens for publishing
| Instagram Login | Facebook Login | |
|---|---|---|
| Host | graph.instagram.com | graph.facebook.com |
| Token | Instagram User access token | Page access token |
| Permissions | instagram_business_basic, instagram_business_content_publish | instagram_basic, instagram_content_publish, pages_read_engagement |
| If the Page role comes via Business Manager | no | Also ads_management, ads_read |
| Access level | Standard for your own accounts; Advanced (App Review) for others | Standard for your own accounts; Advanced (App Review) for others |
yesno
Supported media types and requirements
| Type | How to create the container | Key requirements |
|---|---|---|
| Image | image_url | JPEG only (MPO and JPS not supported), up to 8 MB, aspect ratio between 4:5 and 1.91:1 |
| Reel | media_type=REELS + video_url | 3 seconds to 15 minutes, up to 300 MB, H.264 or HEVC, 23–60 fps; share_to_feed controls Feed visibility |
| Carousel | Child containers with is_carousel_item=true, then media_type=CAROUSEL + children | Up to 10 images, videos or a mix; counts as one post against the limit |
| Story | media_type=STORIES + image or video URL | Disappears after 24 hours; link, poll and location stickers aren't supported; business accounts only per Meta's Facebook Login docs |
Notes and limits
Captions: up to 2,200 characters, 30 hashtags and 20 @-mentions. Image posts accept alt_text (since March 24, 2025; not for Reels or stories), and since June 22, 2026 is_ai_generated=true applies the AI info label. media_type=VIDEO has been unsupported since November 9, 2023 — single videos are published as REELS.
How to publish a post via the Instagram API, step by step
Host the media publicly
Put the file on a public HTTPS URL with no authentication. Meta cURLs the media during the publishing attempt, so the URL must work at that exact moment.
Create the container
POST /<IG_ID>/media with image_url (or media_type=REELS and video_url) and an optional caption. The response is the container ID.
Wait for processing
For videos and carousels, GET /<CONTAINER_ID>?fields=status_code until it returns FINISHED. Meta recommends polling at most once per minute for no more than 5 minutes.
Publish
POST /<IG_ID>/media_publish with creation_id=<CONTAINER_ID>. The ID in the response is the published Instagram media ID.
Read the result
Request the media fields (such as permalink) by the new ID and store a link to the post.
What the raw requests look like
Platform-level calls against the Instagram Login host. For Facebook Login, switch the host to graph.facebook.com and use a Page token.
# 1. Container for a JPEG image
curl -X POST "https://graph.instagram.com/v23.0/$IG_ID/media" \
-H "Authorization: Bearer $IG_TOKEN" \
-d "image_url=https://cdn.example.com/photo.jpg" \
--data-urlencode "caption=New collection is live"
# -> {"id": "<CONTAINER_ID>"}
# 2. Check status (needed for videos and carousels)
curl -G "https://graph.instagram.com/v23.0/$CONTAINER_ID" \
-H "Authorization: Bearer $IG_TOKEN" -d "fields=status_code"
# 3. Publish
curl -X POST "https://graph.instagram.com/v23.0/$IG_ID/media_publish" \
-H "Authorization: Bearer $IG_TOKEN" -d "creation_id=$CONTAINER_ID"Publishing with the Socialift SDK
import asyncio, os
from socialift import Instagram
CDN = "https://cdn.example.com"
async def main():
token = os.environ["IG_USER_TOKEN"]
async with Instagram.from_env() as ig:
photo = await ig.publish(token, image_url=f"{CDN}/photo.jpg", caption="New collection")
reel = await ig.publish(token, video_url=f"{CDN}/teaser.mp4", is_reel=True, caption="Behind the scenes")
album = await ig.publish_carousel(
token, image_urls=[f"{CDN}/1.jpg", f"{CDN}/2.jpg", f"{CDN}/3.jpg"], caption="Lookbook"
)
post = await ig.media_details(photo["media_id"], token)
print(post.get("permalink"), reel["media_id"], len(album["children"]))
asyncio.run(main())publish creates the container and publishes it, returning container_id and media_id. For videos it polls status_code every 2 seconds for up to 60 seconds and raises ApiError on ERROR or timeout — long Reels may need a retry in your own background job.
Always pass is_reel=True for video: Meta no longer accepts media_type=VIDEO.
publish_carousel creates child containers in parallel, waits for the parent container and publishes. It takes image URLs only, keeps the first 10 and raises ConfigurationError for fewer than 2.
For Facebook Login use Instagram.via_facebook(app_secret=...), the Page token and ig_user_id=. Stories, alt_text, share_to_feed and video carousel items aren't parameters of these methods — call the Graph API directly for those.
How many posts per day can you publish via the API?
Instagram accounts are limited to 100 API-published posts within a moving 24-hour period, and a carousel counts as a single post. There is no midnight reset: the window slides continuously.
Check current usage with GET /<IG_ID>/content_publishing_limit. The response contains quota_usage (posts published in the window) and a config object with quota_total and quota_duration; the optional since parameter takes a Unix timestamp no older than 24 hours. Don't hard-code the number — read quota_total from the response.
Containers have their own ceiling: an account can create 400 containers in a rolling 24 hours. An unpublished container expires after 24 hours, so don't create them far in advance.
Container status codes
| status_code | Meaning | What to do |
|---|---|---|
| IN_PROGRESS | The container is still being processed | Poll again later |
| FINISHED | The container is ready to publish | Call media_publish |
| PUBLISHED | The container's media has already been published | Don't publish it again |
| ERROR | Processing failed | Check the media against the specs and create a new container |
| EXPIRED | Not published within 24 hours | Create a new container |
Common publishing errors and how to avoid them
- The URL needs auth, redirects to a login page or has expired (signed URLs) — Meta can't fetch the file.
- The image is PNG, WebP or another format — convert it to JPEG first.
- The aspect ratio is outside 4:5–1.91:1 — crop before uploading.
- media_publish was called before the video or carousel container reached FINISHED.
- media_type=VIDEO was sent — use REELS for single videos.
- More than 10 carousel children, or a caption over 2,200 characters, 30 hashtags or 20 mentions.
- A bulk import hit the 100-post window — check content_publishing_limit first.
- Expecting shopping tags, filters or story stickers — the API doesn't support them.
Frequently asked questions
How many posts per day can I publish with the Instagram API?
One account can publish 100 posts through the API within a rolling 24 hours, and a carousel counts as one. GET /<IG_ID>/content_publishing_limit returns the current usage and the limit value.
Can I publish Instagram Stories via the API?
Yes, with a container using media_type=STORIES. Link, poll and location stickers aren't supported, and Meta's Facebook Login docs limit story publishing to business accounts. The Socialift SDK has a dedicated method for it, publish_story() (since 1.3.0); publish() only posts to the feed.
Can a carousel mix photos and videos?
Yes. A carousel accepts up to 10 images, videos or a mix, each created as its own container with is_carousel_item=true. The SDK's publish_carousel, however, works with images only.
Why does Instagram reject my image?
Usually the format: only JPEG is accepted, up to 8 MB, with an aspect ratio from 4:5 to 1.91:1. The other common cause is a URL that is private or wasn't reachable during publishing, since Meta downloads the file at that moment.
Can I schedule Instagram posts through the API?
The documented flow publishes when you call media_publish, so scheduling is normally built in your own backend with a queue or cron. Containers expire after 24 hours — create them close to the publish time.