Publishing to Instagram through the API is a two-step call: create a media container with POST /<IG_ID>/media, then publish it with POST /<IG_ID>/media_publish. The hard parts are not the calls. They are the account requirements, Meta’s app review, and the fact that a 200 response from the container step does not mean anything is live yet.
This is written for a developer deciding whether to build this in-house.
What can the Instagram API actually publish?
Meta’s Content Publishing docs list four media_type values for containers: VIDEO, REELS, STORIES and CAROUSEL. A plain single image is the default when you pass image_url with no media_type.
| Format | Supported | Notes from the docs |
|---|---|---|
| Single image | Yes | JPEG, passed as a public image_url |
| Video / Reels | Yes | media_type=REELS with video_url |
| Stories | Yes | media_type=STORIES |
| Carousel | Yes | Up to 10 images, videos or a mix |
Two things surprise people. First, Stories are publishable, but when you read a published story back, media_type returns IMAGE or VIDEO, so you have to request media_product_type to tell what it actually is. Second, carousel images are all cropped to match the first image, defaulting to 1:1, so your cropping decisions are made for you.
The gap that matters most is not a format. It is the account. Publishing requires an Instagram professional account (Business or Creator) connected to a Facebook Page, with instagram_basic, instagram_content_publish and pages_read_engagement granted. A personal Instagram account cannot be published to through the API at all, no matter what your code does. If your users are creators on personal accounts, the integration is dead before you write a line.
Note: Figures here were verified against Meta’s Instagram Platform Content Publishing documentation as of September 2026. Platforms change these without notice.
How does auth work, and how long is app review?
You get a user access token through Facebook Login, then exchange the short-lived token server-side for a long-lived one via GET oauth/access_token with grant_type=fb_exchange_token. Meta documents the long-lived user token as lasting about 60 days. The automatic refresh Meta describes applies to SDK-managed tokens, so if you exchange tokens yourself you need your own refresh or re-auth path before day 60.
App review is required for the publishing permissions before anyone outside your own app roles can use the integration. Meta does not publish a guaranteed review turnaround in these developer docs, so treat the duration as unknown and plan for at least one rejection round. Screencasts are part of the submission, which means you need a working demo before you get approval, on an app that cannot yet serve real users.
What is the publishing call sequence?
- Upload your media somewhere publicly reachable. Meta fetches it by URL, so a signed URL that expires in 60 seconds will fail.
POST /<IG_ID>/mediawithimage_urlorvideo_url,caption, andmedia_typeif it is not a plain image. You get back a container ID.- Poll
GET /<IG_CONTAINER_ID>?fields=status_codeuntil it readsFINISHED. Meta recommends polling once per minute for no more than five minutes. POST /<IG_ID>/media_publishwithcreation_idset to the container ID.- Store the returned media ID. That, not the container ID, is the published post.
# 1. create the container
curl -X POST "https://graph.facebook.com/v23.0/$IG_ID/media" \
-d "image_url=https://example.com/photo.jpg" \
-d "caption=Ship it." \
-d "access_token=$TOKEN"
# -> {"id":"17889455560051444"}
# 2. poll until FINISHED
curl "https://graph.facebook.com/v23.0/17889455560051444?fields=status_code&access_token=$TOKEN"
# 3. publish
curl -X POST "https://graph.facebook.com/v23.0/$IG_ID/media_publish" \
-d "creation_id=17889455560051444" -d "access_token=$TOKEN"
Container status_code can be IN_PROGRESS, FINISHED, ERROR, EXPIRED or PUBLISHED. EXPIRED means the container was not published within 24 hours. For a carousel, you create one container per item with is_carousel_item=true, then a parent container with media_type=CAROUSEL and a comma-separated children list.
What are the rate limits?
The documented publishing limit is direct: Instagram accounts are limited to 100 API-published posts within a 24-hour moving period, and a carousel counts as one post. You can read current usage from GET /<IG_ID>/content_publishing_limit rather than guessing, which is what you should do before a bulk run.
That is a moving window, not a calendar day. If you burn 100 posts at 3pm, you do not get a fresh allowance at midnight. Any queue you build needs to model the window, not a daily counter.
Note: Figures here were verified against Meta’s Instagram Platform Content Publishing documentation as of September 2026. Platforms change these without notice.
What will actually cost you three weeks?
Not the two API calls. These four things:
App review. You cannot ship until Meta approves instagram_content_publish, and you cannot demo cleanly until you have built the thing. Budget for resubmission.
Token refresh. Sixty-day long-lived tokens mean a background job, a failure state in your UI for “this account needs reconnecting”, and email to the user before the token dies rather than after. Skip this and every integration silently stops working two months after launch.
Media hosting and format constraints. Meta pulls media from your URL. That means public hosting, correct content types, and transcoding to what Instagram accepts. Single images are JPEG. Video needs to survive Instagram’s own processing step, which happens after your call returns.
Async failure handling. A 200 on the container call means Meta accepted a job. The post can still fail during processing, and you find out only by polling status_code and seeing ERROR. If your data model has only “published” and “failed”, you will report success for posts that never appeared. Model a processing state and a real terminal check, as covered in our social media scheduling API guide.
Then multiply. Reels and Stories have their own quirks, and if you also want TikTok or LinkedIn you start again with a different auth model, a different upload flow and a different review process.
The short version
- Two calls: create container, then
media_publish. Pollstatus_codein between. - Requires a professional Instagram account linked to a Facebook Page.
- 100 API-published posts per rolling 24 hours; check
content_publishing_limit. - Long-lived tokens last about 60 days. Build refresh before launch.
- App review is mandatory and its duration is not published in the docs.
If you are drafting captions while you build, the free Instagram character counter shows where truncation lands.
Doing this once instead of once per platform
The alternative to writing this per network is one API that already holds the tokens, the containers, the polling and the retries. BulkPublish publishes to 15 platforms through a single REST API and SDK, so an Instagram post and a LinkedIn post are the same call with a different channel ID. Token refresh, the rolling-window limit and the async status check are handled on our side, and a post that fails on one platform is reported as partial rather than a false success. The reference is at /developers/ and the REST integration page is at /integrations/rest-api/.