Instagram publishing usually breaks before Instagram has a post to publish.
The request to media_publish is only the final step. Before that can work, the
account must be eligible, the app must use the right login configuration, the
token must carry the matching permissions, Meta must be able to fetch the
media, and the media container must finish processing.
This guide is for engineers adding Instagram publishing to a SaaS product, automation service, or internal platform. It covers the decisions that have to be correct before the first API-published post can exist.
Start with the login configuration
Meta currently provides two configurations for professional Instagram accounts. They both publish content, but they do not use the same login flow, token, host, or permission names.
| Decision | Instagram Login | Facebook Login for Business |
|---|---|---|
| Eligible Instagram accounts | Business and Creator | Business and Creator linked to a Facebook Page |
| User signs in with | Instagram credentials | Facebook credentials |
| Facebook Page required | No | Yes |
| Access token used for publishing | Instagram User token | Facebook Page token |
| API host | graph.instagram.com | graph.facebook.com |
| Publishing scopes | instagram_business_basic, instagram_business_content_publish | instagram_basic, instagram_content_publish, pages_read_engagement |
| Best fit | Products that need Instagram publishing without requiring a Page | Products that also need Page-linked features such as hashtag search, product tagging, or partnership-ad capabilities |
Meta documents the distinction in its Instagram Platform overview and its separate guides for Instagram Login and Facebook Login.
Do not combine the two permission sets. An Instagram User token with
instagram_business_content_publish belongs to the Instagram Login path. A
Page token with instagram_content_publish belongs to the Facebook Login path.
The similar names hide two different authorization models.
Kavenio uses Instagram Login for its Instagram connection flow. It requests
instagram_business_basic and instagram_business_content_publish, exchanges
the authorization code for a short-lived token, exchanges that for a
long-lived token, and reads the professional account identity from
graph.instagram.com.
A successful test account is not production access
Both login configurations support Business and Creator accounts. Consumer accounts are not eligible for the publishing API.
Account type is only the first gate. Meta also separates app access into two levels:
- Standard Access is for accounts owned or managed by people who have a role on the app. It is useful during development and for an app that serves only accounts its team controls.
- Advanced Access is required when the app serves professional accounts owned by customers who do not have a role on the app. Advanced Access requires App Review and Business Verification.
That distinction matters for SaaS products. Publishing successfully to the developer's own Business account proves the request shape and token work. It does not prove that an unrelated customer can authorize the same app in production. Meta explains this boundary in the access-level section of the Instagram Platform overview.
For Facebook Login, the customer also needs an appropriate task on the Page linked to the Instagram professional account. A user who can view a Page is not automatically allowed to grant content-publishing access for it.
The publish operation is a state machine
Instagram does not accept a binary image or video and immediately return a published post. The normal flow creates a media container first and publishes that container only after it is eligible.
professional account
|
v
POST /{ig-user-id}/media
|
v
media container id
|
v
GET /{container-id}?fields=status_code
|
+--> IN_PROGRESS -- wait and check again
|
+--> ERROR ------- stop and preserve the provider response
|
+--> EXPIRED ----- create a new container
|
v
FINISHED
|
v
POST /{ig-user-id}/media_publish
|
v
published Instagram media idMeta's Content Publishing guide defines five container states:
IN_PROGRESS: Meta is still processing the media.FINISHED: the container is ready formedia_publish.ERROR: processing failed.EXPIRED: the container was not published within 24 hours.PUBLISHED: the container's media has already been published.
Treat those as states, not generic success and failure booleans. Retrying
media_publish while a video is still IN_PROGRESS does not make processing
finish sooner. Reusing an EXPIRED container cannot work. Publishing a
PUBLISHED container again risks turning a recovery path into a duplicate-post
path.
Kavenio follows this sequence for Instagram feed posts, carousels, Reels, and
Stories: create the required container or child containers, poll provider
status, and then call media_publish. The public
Kavenio Instagram documentation
shows the supported content types and request fields.
A minimal image-publishing request
For Instagram Login, a single-image publish starts by creating a container on
graph.instagram.com. Keep the API version configurable rather than copying a
version from an old example.
curl -X POST \
"https://graph.instagram.com/${API_VERSION}/${IG_USER_ID}/media" \
-H "Authorization: Bearer ${INSTAGRAM_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"image_url": "https://cdn.example.com/launch.jpg",
"caption": "Launch day"
}'The response contains a container ID, not the final post ID.
{
"id": "17889455560051444"
}Check the container before publishing it:
curl \
"https://graph.instagram.com/${API_VERSION}/${CONTAINER_ID}?fields=status_code" \
-H "Authorization: Bearer ${INSTAGRAM_ACCESS_TOKEN}"Once the status is FINISHED, publish the container:
curl -X POST \
"https://graph.instagram.com/${API_VERSION}/${IG_USER_ID}/media_publish" \
-H "Authorization: Bearer ${INSTAGRAM_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"creation_id\":\"${CONTAINER_ID}\"}"The ID returned by this final request is the published Instagram media ID. Store it separately from the container ID. They represent different objects and are useful for different recovery checks.
Meta must be able to fetch the media
The image_url and video_url parameters are not upload instructions. They
tell Meta where to fetch the file. At publish time, the URL must be reachable
from the public internet without a session cookie, private-network access,
short-lived browser header, or expired signature.
Before creating a container, verify at least:
- the URL uses HTTPS and resolves publicly;
- redirects end at the media file rather than an HTML login page;
- the response returns the expected
Content-Type; - a signed URL remains valid long enough for Meta to fetch and process it;
- the actual file matches the media metadata your application validated.
For a scheduled post, "public now" is not enough. The URL has to remain public when the scheduled job creates the container.
Carousels add another container layer
A carousel uses one child container for each image or video. After the children
are ready, the app creates a parent CAROUSEL container whose children field
contains those child IDs. The parent container is the one sent to
media_publish.
image/video URLs
|
v
2-10 child containers
|
v
parent CAROUSEL container
|
v
media_publishIf one child fails, the parent is not a partial success. Preserve the failed child's provider response, stop the parent publish, and decide whether the user should replace that item or retry the complete carousel.
Check the publishing limit before a queue reaches it
Meta's current publishing guide states that an Instagram professional account
can publish up to 100 API-published posts in a moving 24-hour period. A carousel
counts as one published post. The API exposes the account's current usage at
/{ig-user-id}/content_publishing_limit.
Query the endpoint instead of treating a copied number as permanent. A scheduler can know that ten posts are due; only the provider can report the current account-level publishing usage.
Diagnose the layer that actually failed
| Symptom | Layer to inspect | Useful next check |
|---|---|---|
| OAuth completes, but the account cannot publish | Account eligibility | Confirm the account is Business or Creator |
| Your account works, but unrelated customers cannot connect | App access | Confirm Advanced Access, App Review, and Business Verification |
The token is accepted, but /media returns a permission error | Login configuration and scopes | Match the token host and permission names to the chosen login flow |
Container creation succeeds, but status stays IN_PROGRESS | Provider processing | Poll deliberately; do not call media_publish early |
Container status is ERROR | Media processing | Preserve the provider response and validate the fetched file |
Container status is EXPIRED | Container lifecycle | Create a new container; do not reuse the old ID |
| Meta cannot fetch the URL | Media hosting | Check public access, redirects, TLS, content type, and expiry |
media_publish rejects a busy account | Publishing limit | Query content_publishing_limit and move queued work |
The important separation is simple: authentication proves who granted access, permissions define what the token may do, and a media container represents work Meta still has to process. Treating all three as one "Instagram API call" makes failures hard to explain and harder to recover from.
Kavenio currently uses the direct Instagram Login path and owns the container sequence for supported Instagram posts. If a product needs Page-linked features available only through Facebook Login, that is a separate integration decision, not an option that should be assumed from a successful publish request.