---
title: "How to post to Instagram via API"
description: "Publish images, reels, carousels and stories with Meta's Instagram API, tested on a real account: setup, the container flow, limits, errors, the hard parts."
url: https://rizamo.com/guides/how-to-post-to-instagram-via-api/
---

# How to post to Instagram via API

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

1. Use an Instagram professional account, business or creator. [Meta's API only serves professional accounts](https://developers.facebook.com/documentation/instagram-platform/overview).
2. Create a Meta app with Instagram Login or Facebook Login and get a token with the publish permission.
3. Put the image or video at a public URL.
4. `POST /<IG_ID>/media` with the URL and caption. Meta returns a container id.
5. Wait until the container's `status_code` is `FINISHED`: seconds for an image, about a minute for a short video.
6. `POST /<IG_ID>/media_publish` with the container id. Meta returns the id of the published post.

Steps 3 to 6 are Meta's [content publishing flow](https://developers.facebook.com/documentation/instagram-platform/content-publishing). Steps 1 and 2 are where most of the work is.

## Instagram Login or Facebook Login

Meta offers [two setups of the same API](https://developers.facebook.com/documentation/instagram-platform/overview). 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 | Instagram | Facebook |
| 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](https://developers.facebook.com/documentation/instagram-platform/overview), [content publishing](https://developers.facebook.com/documentation/instagram-platform/content-publishing), [resumable uploads](https://developers.facebook.com/documentation/instagram-platform/content-publishing/resumable-uploads) and [IG Media](https://developers.facebook.com/documentation/instagram-platform/reference/instagram-media#delete).

With Facebook Login, the person must be able to do admin-equivalent tasks on the linked Page ([overview](https://developers.facebook.com/documentation/instagram-platform/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](https://developers.facebook.com/documentation/instagram-platform/content-publishing)). Meta's Facebook Login page also says [stories are only available to business accounts](https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-facebook-login).

An app uses one login or the other, [not both](https://developers.facebook.com/documentation/instagram-platform/app-review). 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](https://developers.facebook.com/documentation/instagram-platform/overview). 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](https://developers.facebook.com/documentation/instagram-platform/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](https://developers.facebook.com/documentation/instagram-platform/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](https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-instagram-login/business-login)). To test with your own account, the **Generate token** button in the App Dashboard gives you a 60-day token directly ([get started](https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-instagram-login/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](https://developers.facebook.com/documentation/instagram-platform/content-publishing)).

Meta downloads the file itself, so it must be on a [publicly accessible server](https://developers.facebook.com/documentation/instagram-platform/content-publishing) 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](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/media#limitations)).

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](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/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](https://developers.facebook.com/documentation/instagram-platform/content-publishing)). The [IG Container](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/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](https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-facebook-login/get-started)). The examples use v26.0, the latest version when we wrote this.

```bash title="Get your account id"
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>"
```

```bash title="1. Create the container"
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"}
```

```bash title="2. Check it is FINISHED"
curl -s "https://graph.instagram.com/v26.0/17889455560051444?fields=status_code&access_token=$IG_TOKEN"
# {"status_code":"FINISHED","id":"17889455560051444"}
```

```bash title="3. Publish it"
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.

```bash title="1. Create the reel container"
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)
```

```bash title="2. Wait for FINISHED"
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
done
```

```bash title="3. Publish"
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\": \"$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](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/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](https://developers.facebook.com/documentation/instagram-platform/content-publishing)).

```bash title="Items, then the carousel"
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_publish
```

The rules, from [content publishing](https://developers.facebook.com/documentation/instagram-platform/content-publishing), [IG User Media](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/media) and the [error codes](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/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](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/media)).

```json title="Story container body"
{ "media_type": "STORIES", "image_url": "https://example.com/stories/today.jpg" }
```

## Media requirements

From the [IG User Media](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/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](https://developers.facebook.com/documentation/instagram-platform/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](https://developers.facebook.com/documentation/instagram-platform/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:

```bash title="Check the limit"
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](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/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](https://developers.facebook.com/documentation/instagram-platform/overview)).
- Images in formats other than JPEG, and filters ([content publishing](https://developers.facebook.com/documentation/instagram-platform/content-publishing)).
- Story stickers: links, polls and locations ([IG User Media](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/media)).
- Reels inside carousels (same page).
- Product tags and partnership ads labels through Instagram Login ([overview](https://developers.facebook.com/documentation/instagram-platform/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](https://developers.facebook.com/documentation/instagram-platform/reference/instagram-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](https://developers.facebook.com/documentation/instagram-platform/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](https://developers.facebook.com/documentation/instagram-platform/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](https://developers.facebook.com/documentation/instagram-platform/content-publishing/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](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/error-codes):

| 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](https://developers.facebook.com/docs/graph-api/guides/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](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/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](https://developers.facebook.com/documentation/instagram-platform/content-publishing), a User or Page token in the [overview](https://developers.facebook.com/documentation/instagram-platform/overview), a User token in the [IG User Media reference](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/media). We tested Instagram Login only.
- **The permission's name:** the [App Review page](https://developers.facebook.com/documentation/instagram-platform/app-review) writes `instagram_business_content_publishing`. Every other page writes `instagram_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:** `/me` returns `id` and `user_id`, and the publishing calls take `user_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](/#waitlist). What it will support on Instagram: [Instagram on Rizamo](/channels/instagram/).
