You publish to Instagram through the API in two calls: POST /{ig-user-id}/media creates a container that holds your picture or video and caption, and POST /{ig-user-id}/media_publish with that container's creation_id makes it a post. For video the two calls are not enough on their own, because Instagram processes the file in between and the container must report status_code: FINISHED before the second call works. It only works for professional accounts (Business or Creator), never for personal ones, and the media has to sit at a public URL when Instagram fetches it.
That is the whole shape. What costs people days is everything around it: a daily cap that Meta's own pages state two different ways, JPEG-only pictures, a status field a naive script never reads, and error codes that mean three different things depending on the subcode. This guide comes from the code Preflight runs every day to publish to Instagram, so it also says where publishing fails in practice. Facts were checked against Meta's documentation on 8 October 2026.
What you need before the first call
An account Instagram can publish to. The API publishes to Instagram professional accounts: Business or Creator. A personal account cannot be published to by any tool; switching it to Creator or Business is free and done in the Instagram app.
One of two ways to connect it. Meta now offers two integrations, and they differ in host, token and permission names:
| Instagram API with Instagram Login | Instagram API with Facebook Login | |
|---|---|---|
| Host | graph.instagram.com | graph.facebook.com |
| Token | Instagram user access token | Facebook Page access token |
| Publishing permission | instagram_business_content_publish (with instagram_business_basic) | instagram_content_publish (with instagram_basic and pages_read_engagement) |
| Needs a Facebook Page | No | Yes, linked to the Instagram account |
If the user holds a Business Manager role on the Page, Facebook Login also asks for ads_management or ads_read. With Facebook Login, the person connecting needs the MANAGE or CREATE_CONTENT task on the Page, and a Page that requires Page Publishing Authorization or two-factor authentication blocks publishing until that is done.
Access level. Both integrations work at Standard access for accounts that belong to your own app's team. For anyone else's account you need Advanced access, which means Meta's app review with a screencast of the publishing flow.
A token that lives. An Instagram Login token is long-lived for 60 days and can be refreshed (graph.instagram.com/refresh_access_token) only while it is still valid; once it lapses, the user has to authorise again. A Facebook Page token obtained from a long-lived user token does not expire. Preflight refreshes the first kind before it runs out and treats a failed refresh as a reason to ask the user to reconnect, not as a transient error.
The two steps
Step 1: create the container. The parameters depend on what you publish:
- Feed picture:
image_url, optionalcaption, optionalalt_text(up to 1,000 characters, pictures only). - Reel:
media_type=REELS,video_url, optionalcaption,share_to_feed,cover_urlorthumb_offset,trial_params. - Story:
media_type=STORIESwithimage_urlorvideo_url. No caption. - Carousel: one container per item with
is_carousel_item=true(no caption on the children), then a parent withmedia_type=CAROUSEL,childrenas a comma-separated list of up to 10 container IDs, and the caption.
video_url takes a public URL. Large files can be sent with upload_type=resumable to rupload.facebook.com, which Meta documents for Facebook Login apps only.
Step 2: wait. Creating a container returns an ID immediately, and that ID says nothing about whether the media was fetched, decoded and accepted. Read GET /{container-id}?fields=status_code,status until it says:
| status_code | Meaning |
|---|---|
IN_PROGRESS | Still processing. Wait. |
FINISHED | Ready. Publish now. |
ERROR | Processing failed. The status field carries the reason or a subcode. |
EXPIRED | Not published within 24 hours. Create a new container. |
PUBLISHED | Already published. |
Meta recommends polling once a minute for at most five minutes. Pictures are usually ready within seconds; a long reel can take longer than the five minutes, so a worker that insists on finishing in one run will either time out or hold a slot. Preflight waits about 30 seconds for a video inside the publishing job and then parks the container as pending; a separate job looks once a minute and publishes it the moment Instagram reports FINISHED.
Step 3: publish. media_publish with the creation_id returns the media ID.
Why a tool that does not wait reports success and nothing appears. A script that calls media_publish straight after creating a video container gets an error (code 9007, subcode 2207027, "media not ready") at best. A script that swallows the error, or counts the successful container creation as the result, tells the user "published" for a post that was never made. The container call succeeding means only that Instagram accepted the request. Success is a media ID from media_publish.
What each post type accepts
| Type | Media | Limits | Notes |
|---|---|---|---|
| Feed picture | JPEG only | 8 MB; ratio 4:5 to 1.91:1; width 320 to 1,440 px (scaled outside) | sRGB; MPO and JPS not supported. Alt text allowed. |
| Carousel | Pictures and video mixed | 2 to 10 items; every picture is cropped to the first item's ratio, 1:1 by default | No captions or locations on children. Reels cannot be items. Counts as one post. |
| Reel | MP4 or MOV; H.264 or HEVC; AAC | 3 s to 15 min; 300 MB; 23 to 60 fps; ratio 0.01:1 to 10:1, 9:16 recommended; width up to 1,920 px | moov atom at the front, no edit lists, closed GOP. Cover: JPEG up to 8 MB. |
| Story | JPEG, or video with the Reel codecs | Video 3 to 60 s and 100 MB; picture 8 MB; 9:16 recommended | Expires after 24 hours. No caption. No link, poll or location stickers. |
| Trial reel | As a Reel | trial_params with graduation_strategy of MANUAL or SS_PERFORMANCE | Shown to non-followers first; graduated by hand or on performance. |
Three details trip people up. A phone photo is 3:4, which is 0.75, below the 0.8 floor of 4:5, so it is refused; crop it first. A PNG or WebP is refused outright: the API takes JPEG only. And the published object does not say what it was: a Reel comes back with media_type: VIDEO and a story with IMAGE or VIDEO; read media_product_type to tell them apart. For every network side by side, see the limits table.
The daily cap
Meta's documentation gives two numbers for how many posts an account may publish through the API per rolling 24 hours:
- The content publishing guide says 100 in its rate-limit section, and 50 in its carousel section.
- The
media_publishreference and thecontent_publishing_limitreference both say 50.
Treat 50 as the working number, since it is the one the endpoint itself reports (the 100 may reflect an older or planned figure; Meta does not reconcile them). The cap is counted when you call media_publish, not when you create containers, and a carousel counts as one. The window is a moving 24 hours, so a slot frees 24 hours after each individual publish, not at midnight. Creating containers has its own, separate ceiling of 400 per rolling 24 hours.
You do not have to guess. GET /{ig-user-id}/content_publishing_limit?fields=config,quota_usage returns quota_usage (publishes in the last 24 hours, or since a since Unix timestamp no older than 24 hours) and config.quota_total and config.quota_duration (currently 50 and 86,400 seconds). Without fields, only the usage comes back. Meta's own advice is that apps enforce the limit themselves, especially for scheduled posts.
When the limit is hit, media_publish fails with code 9, subcode 2207042. It is not permanent. Preflight treats it as a wait: it retries after three hours, up to 12 attempts, which covers a full day of window rolling, instead of failing the post after five quick retries inside the same window. If you build your own, queue by the quota_usage you read and keep a margin, because other tools publishing to the same account share the same count.
Errors you will meet
Meta's error-code reference lists these for content publishing (all returned as HTTP 400). The third column is what to do, and where Preflight acts on its own, it says so.
| Error | Meaning | What to do |
|---|---|---|
| Code 9, subcode 2207042 | Daily publishing limit reached | Wait for the window to roll. Preflight retries every 3 hours, up to 12 times. |
| Code 9007, subcode 2207027 | Media not ready | Poll status_code until FINISHED, then publish. |
| Code 36000 / 2207004 | Image over 8 MiB | Compress; Preflight converts and fits it first. |
| Code 36001 / 2207005 | Unsupported image format | Send a JPEG. |
| Code 36003 / 2207009 | Aspect ratio out of range | Crop to between 4:5 and 1.91:1. |
| Code 36004 / 2207010 | Caption too long, or too many hashtags or mentions | Max 2,200 characters, 30 hashtags, 20 @mentions (a separate code 100 / 2207040 covers mentions). |
| Code 352 / 2207026 | Unsupported video format | MP4 or MOV, with the codecs above. |
| Code 9004 / 2207052 | Media URL could not be fetched | URL must be public, reachable by Meta, and ASCII. |
| Code 24 / 2207006 | Media not found | Often a missing permission or expired token; create a new container. |
| Code 24 / 2207008 | Container does not exist or expired | Retry once or twice over 30 s to 2 min, then create a new container. |
| Code -1 / 2207032, -1 / 2207053 | Media creation or upload failed (53 mostly on video) | Create a new container and retry. |
| Code 100 / 2207028 | Carousel with fewer than 2 or more than 10 items | Send 2 to 10. |
| Code 25 / 2207050 | Account inactive, checkpointed or restricted | The owner must sign in to the Instagram app and resolve it. |
| Code 190 | Token expired or invalid | Reconnect the account. Preflight flags it and stops retrying. |
| Code 200 or 10 | Missing permission | Add the scope and reconnect. Not retried. |
| Code 100 | Bad argument | Read error_user_msg. Not retried. |
| Codes 4, 17, 32, 613 | Rate limiting (app, user, Page, call) | Back off; these are measured over an hour. Preflight waits 30 minutes, up to 8 attempts. |
When status_code is ERROR, the status field holds a subcode you can look up in Meta's error codes page. Preflight splits those into two groups: a reason Meta names is the file's fault, so it is permanent and shown to the user; an unnamed "unknown" failure is usually a dropped transcode on Meta's side, which is retried.
What the API will not do
- Schedule natively. None of the Instagram endpoints takes a publish time.
scheduled_publish_timeis a Facebook Page feature; Meta's publishing guide andmedia_publishreference mention no scheduling at all. Every Instagram scheduler, Preflight included, keeps the post itself and calls the API at the chosen minute. Store the file and the caption, not the container, which expires after 24 hours. - Tag people, places and products from every app.
user_tags(public accounts, with coordinates on pictures),location_id(a Page ID with location data) andproduct_tags(Shop catalogue, extra permissions) exist in the reference.collaboratorstakes up to three usernames. Preflight sends collaborators, alt text, trial reels, the reel cover and share-to-feed, and does not send user tags, locations, product tags, the AI-generated label or audio names. - Music. There is no way to attach a track from Instagram's library;
audio_nameonly renames the audio of your own Reel, once. - Edit a caption after publishing. The publishing endpoints list no update. Preflight marks Instagram as not editable.
- Delete. The publishing references say deletion is unsupported. In practice Preflight's code issues
DELETE /{media-id}for accounts connected through a Facebook Page, which needsinstagram_manage_contents, and tells users that accounts connected directly with Instagram Login cannot delete through the API. We could not find that call in Meta's current reference pages, so treat it as observed behaviour, not documented behaviour, and test it before you promise it. - Personal accounts. Not at all, by any app.
How Preflight handles it
Everything above runs before and around the two calls. Before upload, Preflight checks the caption length, 30 hashtags and 20 mentions, the 4:5 to 1.91:1 ratio, Reel length and frame rate, story length and size, and the 10-item carousel. A picture that is not a JPEG or is over 8 MB is converted once: turned upright, fitted to 1,440 pixels wide, transparency put on white, and the JPEG handed to Instagram while the original stays in your library. Containers are polled, videos are parked and resumed, transient errors are retried with the delay Meta asks for, token failures are flagged for reconnecting, and the daily cap becomes a three-hour wait. Preflight does not call content_publishing_limit ahead of time; it reacts to code 9, subcode 2207042, which is the one answer the API cannot get wrong.
These checks run on every plan, the free one included, and the same rules are available through the API for tools that post on your behalf. To connect an account and see what goes out, start at the Instagram integration page; the full rule list is on Instagram limits, and how scheduling works in the product is in scheduling Instagram posts.
Checked against Meta's Instagram content publishing guide, the IG User media reference and the content_publishing_limit reference on 8 October 2026, and against Preflight's own publishing code.
Questions people ask
- Can you post to Instagram through the API?
- Yes, to Business and Creator accounts, in two calls: create a media container with POST /{ig-user-id}/media, wait until its status_code is FINISHED, then call POST /{ig-user-id}/media_publish with the container's ID. Feed pictures, carousels, Reels and stories are supported; the media must be at a public URL and pictures must be JPEG.
- Is there a daily limit on Instagram API posts?
- Yes, counted per account over a rolling 24 hours and enforced on media_publish. Meta's publishing guide states 100 in one place and 50 in another, while the content_publishing_limit and media_publish references say 50. Read the live value from content_publishing_limit (config.quota_total) and treat 50 as the working number. A carousel counts as one post, and the failure is code 9, subcode 2207042.
- Do I need a Business account?
- You need a professional account: Business or Creator. A personal account cannot be published to by the API or by any tool. Depending on the integration, the account is connected through a linked Facebook Page (Facebook Login) or directly (Instagram Login), and your app needs the matching content publish permission and, for other people's accounts, Advanced access.
- Can the Instagram API schedule posts?
- Not natively. Meta's Instagram publishing endpoints have no scheduled publish time parameter, and containers expire after 24 hours, so a scheduler stores the post itself and runs the two calls at the chosen time. Preflight does exactly that, and respects the daily cap while doing it.
- Why did my container never publish?
- Usually one of four reasons: the video was still IN_PROGRESS when you called media_publish (error 2207027), so you must poll status_code until FINISHED; the status turned ERROR because the file or format was rejected, so read the status field; the container was older than 24 hours and EXPIRED; or the account was at its daily publishing limit. Creating a container only means Instagram accepted the request, not that a post exists.