Instagram has an official API for publishing images, reels, carousels and stories to business and creator accounts. This guide walks through Meta's own API as documented in October 2026, with curl for every step, and we ran each step on a real business account. Claims about the API link to Meta's docs; what we saw in our own test says so.
The short version
- Use an Instagram professional account, business or creator. Meta's API only serves professional accounts.
- Create a Meta app with Instagram Login or Facebook Login and get a token with the publish permission.
- Put the image or video at a public URL.
POST /<IG_ID>/mediawith the URL and caption. Meta returns a container id.- Wait until the container's
status_codeisFINISHED: seconds for an image, about a minute for a short video. POST /<IG_ID>/media_publishwith the container id. Meta returns the id of the published post.
Steps 3 to 6 are Meta's content publishing flow. Steps 1 and 2 are where most of the work is.
Instagram Login or Facebook Login
Meta offers two setups of the same API. Both publish feed posts, reels, carousels and stories. They differ in how people log in and in a few extras.
| Instagram API with Instagram Login | Instagram API with Facebook Login | |
|---|---|---|
| Accounts | Professional (business or creator) | Professional, linked to a Facebook Page |
| People log in with | ||
| Host | graph.instagram.com |
graph.facebook.com |
| Publish permissions | instagram_business_basic, instagram_business_content_publish |
instagram_basic, instagram_content_publish, pages_read_engagement |
| Upload a video file directly | No | Yes, resumable upload |
| Delete a published post | No | Yes |
| Product tags, partnership ads | No | Yes |
Sources: the overview, content publishing, resumable uploads and IG Media.
With Facebook Login, the person must be able to do admin-equivalent tasks on the linked Page (overview). If they have their Page role through Business Manager, your app also needs ads_management or ads_read, and if the Page requires Page Publishing Authorization, publishing fails until it is complete (content publishing). Meta's Facebook Login page also says stories are only available to business accounts.
An app uses one login or the other, not both. If the account has no Facebook Page, Instagram Login is the only option. If you need to delete posts or send video files instead of URLs, you need Facebook Login.
Permissions and app review
Meta has two access levels. Standard Access is the default, and it is all you need when your app only serves Instagram accounts you own or manage: no App Review. To serve other people's accounts, your app needs Advanced Access, which requires App Review and Business Verification.
For App Review you describe how you use each permission and upload a screencast of the full flow, plus test credentials and step-by-step instructions for the reviewers. Some permissions also need at least one successful API call before you can request Advanced Access (App Review).
Tokens from Business Login for Instagram last one hour. Exchange them on your server for a long-lived token that lasts 60 days and can be refreshed once it is at least 24 hours old (Business Login for Instagram). To test with your own account, the Generate token button in the App Dashboard gives you a 60-day token directly (get started).
How publishing works
Publishing takes two calls. First you create a container: you send the media URL and caption to /<IG_ID>/media, and Meta fetches the file. Then you publish the container with /<IG_ID>/media_publish (content publishing).
Meta downloads the file itself, so it must be on a publicly accessible server at the moment of the call. A URL behind a login will not work.
Containers expire after 24 hours, and an account can create at most 400 of them in a rolling 24 hours (IG User Media).
Every container is processed in the background, images included. Getting a container id does not mean the upload worked: read the container's status_code and publish only when it is FINISHED (IG User Media). In our test, an image published right after its container was created failed with 9007 / 2207027 (not ready), and the same call worked seconds later. Meta recommends checking once a minute for no more than five minutes (content publishing). The IG Container reference lists the values:
status_code |
Meaning |
|---|---|
IN_PROGRESS |
Still processing |
FINISHED |
Ready to publish |
PUBLISHED |
Already published |
ERROR |
Failed. The container's status field holds the error subcode |
EXPIRED |
Not published within 24 hours |
If a publish call times out, read the container's status before you try again. PUBLISHED means the post is already live.
Post an image with curl
The examples use Instagram Login, so the host is graph.instagram.com and the token is an Instagram User access token. With Facebook Login, the same calls go to graph.facebook.com, and you find the account id with GET /<PAGE_ID>?fields=instagram_business_account (get started with Facebook Login). The examples use v26.0, the latest version when we wrote this.
export IG_TOKEN="<INSTAGRAM_USER_ACCESS_TOKEN>"
curl -s "https://graph.instagram.com/v26.0/me?fields=user_id,username&access_token=$IG_TOKEN"
export IG_ID="<USER_ID_FROM_THE_RESPONSE>"curl -s -X POST "https://graph.instagram.com/v26.0/$IG_ID/media" \
-H "Authorization: Bearer $IG_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"image_url": "https://example.com/photos/autumn-menu.jpg",
"caption": "Autumn menu is here. #coffee"
}'
# {"id":"17889455560051444"}curl -s "https://graph.instagram.com/v26.0/17889455560051444?fields=status_code&access_token=$IG_TOKEN"
# {"status_code":"FINISHED","id":"17889455560051444"}curl -s -X POST "https://graph.instagram.com/v26.0/$IG_ID/media_publish" \
-H "Authorization: Bearer $IG_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "creation_id": "17889455560051444" }'
# {"id":"17920238422030506"}The second id is the published post.
Post a reel
A reel is a video container with media_type set to REELS. Video takes longer to process, so the loop below checks once a minute until it is FINISHED, using jq to read the JSON. Our 20-second test reel took about a minute, and a video with no audio track published fine.
CONTAINER_ID=$(curl -s -X POST "https://graph.instagram.com/v26.0/$IG_ID/media" \
-H "Authorization: Bearer $IG_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"media_type": "REELS",
"video_url": "https://example.com/videos/autumn.mp4",
"caption": "Autumn menu is here.",
"share_to_feed": true
}' | jq -r .id)for i in 1 2 3 4 5; do
STATUS=$(curl -s "https://graph.instagram.com/v26.0/$CONTAINER_ID?fields=status_code&access_token=$IG_TOKEN" | jq -r .status_code)
echo "$STATUS"
case "$STATUS" in FINISHED|ERROR|EXPIRED) break ;; esac
sleep 60
donecurl -s -X POST "https://graph.instagram.com/v26.0/$IG_ID/media_publish" \
-H "Authorization: Bearer $IG_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"creation_id\": \"$CONTAINER_ID\"}"With share_to_feed set to true, the reel can appear in Feed as well as Reels. Neither value guarantees it appears in the Reels tab. cover_url sets a cover image, and thumb_offset picks a frame in milliseconds instead (IG User Media).
Post a carousel
A carousel is one container per item, each created with is_carousel_item set to true, then a parent container with media_type set to CAROUSEL and the item ids in children. You publish the parent, once its status_code is FINISHED like any other container (content publishing).
for URL in https://example.com/photos/1.jpg https://example.com/photos/2.jpg; do
curl -s -X POST "https://graph.instagram.com/v26.0/$IG_ID/media" \
-H "Authorization: Bearer $IG_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"image_url\": \"$URL\", \"is_carousel_item\": true}" | jq -r .id
done
# 18011111111111111
# 18022222222222222
curl -s -X POST "https://graph.instagram.com/v26.0/$IG_ID/media" \
-H "Authorization: Bearer $IG_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"media_type": "CAROUSEL",
"children": "18011111111111111,18022222222222222",
"caption": "Two new drinks for autumn."
}'
# Publish the id this returns with media_publishThe rules, from content publishing, IG User Media and the error codes:
- A carousel needs at least 2 items, images, videos or a mix. Meta's pages disagree on the maximum (20, or 10 in one error message). In our test, an 11-item carousel container was accepted and processed; we did not publish it.
- Every image is cropped to the first image's aspect ratio, 1:1 by default.
- The caption goes on the parent. Items cannot have their own caption or location.
- Reels cannot be carousel items.
- The AI label (
is_ai_generated) goes on the parent only. Setting it on an item is an error.
Post a story
A story is a container with media_type set to STORIES and an image_url or a video_url. Stories expire after 24 hours, and the API cannot add stickers such as links, polls or locations. Mentioning users is supported (IG User Media).
{ "media_type": "STORIES", "image_url": "https://example.com/stories/today.jpg" }Media requirements
From the IG User Media reference:
| Media | Format | Max size | Shape | Length |
|---|---|---|---|---|
| Feed image | JPEG, sRGB | 8 MB | 4:5 to 1.91:1, 320 to 1440 px wide | |
| Story image | JPEG, sRGB | 8 MB | 9:16 recommended | |
| Reel | MP4 or MOV, H.264 or HEVC, AAC audio | 300 MB | 9:16 recommended, up to 1920 px wide | 3 s to 15 min |
| Story video | MP4 or MOV, H.264 or HEVC, AAC audio | 100 MB | 9:16 recommended, up to 1920 px wide | 3 s to 60 s |
| Reel cover | JPEG, sRGB | 8 MB | 9:16 recommended |
Video also needs the moov atom at the front of the file, no edit lists, 23 to 60 frames per second, closed GOP, 4:2:0 chroma subsampling and at most 25 Mbps. Images outside the width range are scaled. Captions take up to 2,200 characters, 30 hashtags and 20 @ tags.
JPEG is the only image format. Extended JPEG formats such as MPO and JPS are not supported (content publishing).
The daily publishing limit
Each account can publish a limited number of posts through the API in a moving 24-hour window. A carousel counts as one post, and the limit is enforced when you call media_publish. Meta recommends that your app enforce the limit too, especially if it schedules posts (content publishing).
Meta's pages give different numbers for this limit (50 in some places, 100 in others), so read it from the API instead of hardcoding it:
curl -s "https://graph.instagram.com/v26.0/$IG_ID/content_publishing_limit?fields=quota_usage,config&access_token=$IG_TOKEN"quota_usage is how many posts the account published in the last 24 hours, or since the since timestamp you pass. config.quota_total is the limit, and config.quota_duration is the window in seconds (content publishing limit).
When we checked in October 2026, our account returned a quota_total of 100 for a quota_duration of 86400. The count can lag: right after our fourth post (an image, a reel, a carousel and a story) quota_usage read 3, and later 4. Stories count.
What the API cannot publish
According to Meta's pages:
- Posts from personal accounts. Only professional accounts are served (overview).
- Images in formats other than JPEG, and filters (content publishing).
- Story stickers: links, polls and locations (IG User Media).
- Reels inside carousels (same page).
- Product tags and partnership ads labels through Instagram Login (overview).
- Edits after publishing. The only change the API makes to a published post is turning comments on or off. Deleting works through Facebook Login only, and a carousel is deleted as a whole (IG Media).
Is Instagram's API free?
Meta's Instagram Platform documentation lists no price for calling the API. The cost is in access and limits. Serving accounts you don't own or manage takes Advanced Access, which needs App Review and Business Verification (overview), and each account has the daily publishing limit above. If you only publish to your own accounts, Standard Access works without review.
Does Instagram have an upload API?
For images, no. Image containers take an image_url, and Meta fetches the file from it, so it must be public when you call (content publishing).
For video, yes, with Facebook Login. Create the container with upload_type=resumable, then send the file, or the URL of a hosted file, to rupload.facebook.com (resumable uploads). Apps on Instagram Login send a video_url.
Common errors
Meta's errors carry a code and an error_subcode. These are the ones you are most likely to see when publishing, from Meta's error codes reference:
| Code / subcode | What it means | What to do |
|---|---|---|
9004 / 2207052 |
Meta could not fetch the media from the URL | Make sure the URL is valid and public |
9007 / 2207027 |
The media is not ready to publish | Check the container and publish when it is FINISHED |
9 / 2207042 |
The account reached its daily publishing limit | Try again the next day |
4 / 2207051 |
The action looks like spam to Meta | Let Meta know if it is a mistake |
24 / 2207008 |
The container does not exist or has expired | Retry once or twice within 2 minutes, then create a new container |
36003 / 2207009 |
Image aspect ratio out of range | Use 4:5 to 1.91:1 |
36000 / 2207004 |
Image too large | Keep it under 8 MiB |
36004 / 2207010 |
Caption too long | 2,200 characters, 30 hashtags, 20 @ tags at most |
352 / 2207026 |
Video format not supported | Send MP4 or MOV |
25 / 2207050 |
The account is restricted | The owner signs in to the Instagram app and completes what it asks |
-1 / 2207001 |
Instagram server error | Try again |
Code 190 comes from the Graph API itself: the access token has expired or been revoked, and you need a new one (Graph API error handling).
One success is partial. media_publish can return a media id together with config_issue: CAPTION_NOT_ATTACHED means the post went out without its caption, and USER_TAGGING_FAILURE without its user tags (media publish). Meta says to treat it as a signal to reconcile, not as an error: the post is live, so do not publish it again.
What it takes in production
The calls above publish one post. Publishing for many accounts, every day, adds this:
| The hard part | Why | What you end up building |
|---|---|---|
| Two logins | Different hosts and features, and three answers on which Facebook token to use | Support for one login or both |
| Access | App Review and Business Verification before you serve anyone else's account | A demo a reviewer can follow, and time to wait |
| Tokens | One hour, then 60 days, for every account | A refresh job and a way to ask people to reconnect |
| Media | Every file at a public URL, and JPEG only for images | Short-lived public hosting and a conversion step |
| Containers | Images need processing too, not only video | A job that waits and checks each post |
| Timeouts | A publish call that timed out may have published | Retries that read the container first |
| Daily limit | 50 or 100 in the docs, and the counter lags | A count per account before each publish, with a margin |
| Format rules | Written differently on different pages | Your own validation, tested against the API |
| After publishing | No edits, and deletes only through Facebook Login | Checks before publishing |
Where Meta's pages disagree, beyond what the sections above cover:
- Facebook Login's token: a Page token in the publishing guide, a User or Page token in the overview, a User token in the IG User Media reference. We tested Instagram Login only.
- The permission's name: the App Review page writes
instagram_business_content_publishing. Every other page writesinstagram_business_content_publish, the spelling to put in your login URL. children: "a comma separated list" in the publishing guide, an array in the reference. A comma-separated string worked.- Two ids:
/mereturnsidanduser_id, and the publishing calls takeuser_id.
Where Rizamo fits
If you only publish to your own account, the calls above are all there is: Standard Access needs no review. For everything in the table above, Rizamo is a social media API and MCP server built to take this layer off your hands.
| The hard part | What Rizamo is built to do |
|---|---|
| Two logins | Connect an account with either; Facebook Login when it has a Page |
| Access | Accounts connect through Meta apps that already passed App Review, so you do not submit your own |
| Tokens | Refresh each account's token before it expires, check account health daily, ask for a reconnect when needed |
| Media | Take a file or a URL, store it, serve it to Meta, make the JPEG Instagram needs |
| Containers | One call to publish: Rizamo creates the container, waits for it and publishes |
| Timeouts | Never repeat a publish blindly: look for the post first. An Idempotency-Key on every request |
| Daily limit | Read each account's quota and count before publishing. Over it, that post fails with daily_limit_reached and the others still go out |
| Format rules | Check captions, media count, ratios and lengths when you create the post |
| Errors | One error shape, with Meta's code and subcode and the action to take |
Rizamo is opening soon: join the waitlist. What it will support on Instagram: Instagram on Rizamo.