API GuidesAugust 2, 202625 min readingMarcel Czuryszkiewicz

Instagram Graph API: A Production Guide for Multi-Account Integrations

Two-step container publishing, Reels and Stories constraints, impressions-derived rate limits, and the daily publishing cap that Meta's own documentation contradicts. Plus the part no other guide covers: what breaks when you run a six-figure fleet of connected accounts instead of one.

instagram graph api The Instagram Graph API is the HTTP interface for publishing to and reading from Instagram professional accounts - Business and Creator. It runs on Meta's Graph API infrastructure (current version v26.0), it cannot touch personal accounts, and publishing is always two steps: create a media container, then publish it. Rate limits are derived from the account's impressions, not from a plan you buy. There is a separate daily publishing cap that you should query rather than assume.

This guide covers the parts that break in production, and one thing no other guide covers: what happens to all of the above when you have a six-figure fleet of client accounts connected instead of one. The numbers here come from running about 227,000 of them.

What the API does and does not do

The first column of this table is why teams start. The second column is where the plan changes.

You want toSupported?Notes
Publish images, videos, Reels, carousels to a professional accountYesTwo-step container flow, always
Publish StoriesYesmedia_type=STORIES; docs note Stories publishing is restricted to business accounts, not all professional accounts
Publish to a personal Instagram accountNoThe API explicitly cannot access non-Business, non-Creator accounts. No workaround exists
Schedule a post nativelyNoThere is no scheduled_publish_time equivalent. You hold the schedule; the API publishes now
Publish with a filterNoFilters are not supported
Upload PNG or WebPNoJPEG only for images. Extended JPEG (MPO, JPS) also rejected
Add shopping tagsNo on the publishing flowShopping tags unsupported; product_tags is a separate, narrower mechanism
Read comments, reply, hide, deleteYesinstagram_manage_comments / instagram_business_manage_comments
Hashtag search, mentions, business discoveryYes - Facebook Login onlyNot available on the Instagram Login configuration
Product tagging and Partnership AdsYes - Facebook Login onlyInstagram Login "cannot access ads or tagging"
Serve accounts you do not ownRequires Advanced AccessStandard Access reaches only assets on your app

Three of these deserve a sentence more, because they are where sprints go to die.

There is no personal-account publishing, and there never will be. If your product plan says "let users post to their Instagram," the user has to convert to a professional account first. Build that conversion prompt into onboarding; do not discover it during a customer demo.

There is no native scheduling. Facebook Pages have scheduled_publish_time. Instagram does not. Every "schedule an Instagram post" feature in every tool is a job queue holding the payload until the minute arrives and then calling the API. That is your problem to build, and it interacts badly with the publishing cap - covered below.

Standard Access looks like it works. Every call succeeds against your own test account. Point the same code at a customer's account and you get empty arrays or permission errors. That is not a bug. Advanced Access requires App Review and Business Verification. Same trap as the Facebook Graph API, same fix.

Two logins, two hosts, two token lifecycles

Meta ships two configurations of the Instagram API, and they are not variants of one thing. They use different hosts, different token types, different permission names, and different refresh mechanics. Choosing wrong costs you a rewrite.

Instagram API with Instagram LoginInstagram API with Facebook Login
Auth flowBusiness Login for InstagramFacebook Login for Business
Hostgraph.instagram.comgraph.facebook.com (+ rupload.facebook.com)
Token typeInstagram User access tokenFacebook User or Page access token
Facebook Page requiredNoRequired
User logs in withInstagram credentialsFacebook credentials
Publishing permissioninstagram_business_content_publishinstagram_content_publish
Basic permissioninstagram_business_basicinstagram_basic + pages_read_engagement + pages_show_list
Hashtag search
Product tagging / Partnership Ads
InsightsMedia insightsMedia + account insights
Long-lived token refreshDedicated /refresh_access_token endpointPage token derived from a long-lived user token
Resumable video upload✓ (upload_type=resumable)

Pick Instagram Login if you only publish and moderate comments, and you want the shortest onboarding - no Facebook Page, no Business Manager role checks, one fewer thing for the customer to get wrong.

Pick Facebook Login if you need hashtag search, product tagging, account-level insights, or resumable uploads for large video files. Also pick it if the customer already connects their Facebook Page to you, because then one consent screen covers both.

In practice, agencies serving many brands end up with both, which is the multi-tenant complication discussed later.

The Instagram Login flow, end to end

# 1. Send the user to the authorization window
https://www.instagram.com/oauth/authorize
  ?client_id=990602627938098
  &redirect_uri=https://app.example.com/callback
  &response_type=code
  &scope=instagram_business_basic,instagram_business_content_publish
  &state=csrf_nonce_here

The redirect returns ?code=...#_. Strip the trailing #_ - it is not part of the code. The code is valid for one hour and single-use.

# 2. Exchange the code for a short-lived token (server-side only)
curl -X POST https://api.instagram.com/oauth/access_token \
  -F 'client_id=990602627938098' \
  -F 'client_secret=a1b2C3D4' \
  -F 'grant_type=authorization_code' \
  -F 'redirect_uri=https://app.example.com/callback' \
  -F 'code=AQBx-hBsH3...'
{
  "data": [
    {
      "access_token": "EAACEdEose0...",
      "user_id": "1020...",
      "permissions": "instagram_business_basic,instagram_business_content_publish"
    }
  ]
}

Two things to notice. The response is wrapped in a data array - unlike the Facebook token endpoints, which return a bare object. And permissions tells you exactly what the user actually granted, which may be less than what you asked for. Read it and store it. A user who unticked content publishing on the consent screen will produce a permission error on their first scheduled post, hours later, with no obvious cause.

# 3. Exchange for a long-lived token - 60 days
curl -G "https://graph.instagram.com/access_token" \
  -d "grant_type=ig_exchange_token" \
  -d "client_secret=${APP_SECRET}" \
  -d "access_token=${SHORT_LIVED_TOKEN}"
{ "access_token": "EAACEdEose0...", "token_type": "bearer", "expires_in": 5183944 }
# 4. Refresh before day 60
curl -G "https://graph.instagram.com/refresh_access_token" \
  -d "grant_type=ig_refresh_token" \
  -d "access_token=${LONG_LIVED_TOKEN}"

The refresh has three preconditions that are easy to violate:

  • The existing token must be at least 24 hours old. A refresh loop that fires immediately after issuance fails.
  • The token must not have expired. There is no grace period - a token unrefreshed for 60 days is dead, and the user has to re-authorize.
  • The user must still have granted instagram_business_basic.

The short-lived token, incidentally, is also valid for only one hour. Exchange it in the callback handler, not in a nightly job.

For the Facebook Login path, token mechanics are the Page-token mechanics we cover in Facebook Page access tokens - no /refresh_access_token, a different failure taxonomy, and role changes in Business Manager that silently invalidate access.

Publishing: the two-step container flow

steps.jpg

Every Instagram publish is two API calls minimum. There is no single-shot endpoint.

POST /{ig-user-id}/media          → container id
POST /{ig-user-id}/media_publish  → media id

Step 1 - create the container

curl -X POST "https://graph.instagram.com/v26.0/${IG_USER_ID}/media" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -d '{
        "image_url": "https://cdn.example.com/launch.jpg",
        "caption": "Launch day.",
        "alt_text": "A laptop showing a deployment dashboard"
      }'
{ "id": "17889455560051444" }

That is a container ID, not a media ID. It is not a post. Nothing is visible on Instagram yet.

The critical constraint on image_url and video_url: Meta cURLs the URL from its own servers at the moment of the call. The media must be on a publicly reachable host. Signed URLs with short TTLs, staging environments behind basic auth, and buckets with IP allowlists all fail here - and they fail with a generic fetch error, not a helpful one.

Step 2 - publish

curl -X POST "https://graph.instagram.com/v26.0/${IG_USER_ID}/media_publish" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -d '{ "creation_id": "17889455560051444" }'
{ "id": "17920853729530539" }

Now it is live.

The 24-hour expiry, and why it changes your queue design

A container that is not published within 24 hours expires. This single fact invalidates the obvious scheduler design.

The naive implementation creates the container when the user hits "schedule", stores the container ID, and publishes at the appointed time. That works for a post scheduled two hours out. It fails silently for one scheduled next Tuesday.

The correct design: store the payload, create the container at publish time. Container creation is part of the publish job, not part of the scheduling request. Cost: your job now makes two or three calls instead of one, and validation errors surface at publish time instead of at schedule time. Mitigate by validating media dimensions and caption length synchronously when the user schedules, and creating the container only in the worker.

Polling the container status

Video containers are not immediately publishable - Meta transcodes first. Publishing too early returns error 9007 / 2207027, "The media is not ready for publishing."

curl -G "https://graph.instagram.com/v26.0/${CONTAINER_ID}" \
  -d "fields=status_code" \
  -d "access_token=${ACCESS_TOKEN}"
{ "status_code": "IN_PROGRESS", "id": "17889455560051444" }
status_codeMeaningWhat to do
IN_PROGRESSStill processingKeep polling
FINISHEDReadyCall media_publish
PUBLISHEDAlready liveStop. Do not publish again
ERRORProcessing failedTerminal. Recreate the container from source
EXPIREDNot published within 24 hTerminal. Recreate from source

Meta's guidance is to poll once per minute for no more than five minutes. In our experience that is right for images and short videos and optimistic for long Reels; treat five minutes as a soft floor for your timeout, not a hard ceiling.

The idempotency problem nobody writes about

media_publish can succeed on Meta's side and still return a 500 to you - typically code: 2, is_transient: true, "An unexpected error has occurred." A socket timeout does the same thing.

Retrying blindly publishes the post twice. Not retrying abandons a post that may have failed for real. Both are wrong.

The resolution is that the container knows. Before retrying a failed media_publish, read the container's status_code. If it is PUBLISHED, the post is live - you simply never received the media ID. Treat the container ID as your correlation key and move on.

on media_publish failure:
    status = GET /{container-id}?fields=status_code   # never let this throw
    if status == "PUBLISHED":  return success         # already live, do not retry
    if status in ("ERROR", "EXPIRED"):  fail terminally
    else: retry with backoff

If the status lookup itself fails, treat it as "unknown" and retry - the duplicate risk is real but smaller than the risk of dropping a customer's post. This is the container flow's one genuinely subtle piece of engineering, and it is invisible until you are publishing at volume.

This is not a rare edge case. In our traffic, about 10% of media_publish errors are false failures - the call returned an error, but the container was already PUBLISHED and the post was live on Instagram.

One in ten. If you retry blindly on media_publish errors, roughly a tenth of those retries are attempts to publish something that already exists, and the ones that succeed produce a visible duplicate on your customer's feed. The status check above costs one extra API call on a path that is already failing, and it is the difference between an error rate and a credibility problem.

(https://media.bundle.social/container_publication_a3df62460a.jpg)

Reels, Stories, and carousels

Same two-step flow, different parameters and different constraints.

Image postReelStoryCarousel
media_typeomit (default)REELSSTORIESCAROUSEL
Media parameterimage_urlvideo_urlimage_url or video_urlchildren
Container calls111n + 1
Caption✓ (on the parent)
alt_text✓ (per child)
cover_url / thumb_offset---
share_to_feed---
collaborators (max 3)
user_tags✓ (x/y)✓ (per child)
location_id
Reported media_type after publishIMAGEVIDEOIMAGE/VIDEOCAROUSEL_ALBUM
Duration-3 s – 15 min3–60 s (video)-
Max file size8 MB image300 MB100 MB videoper child

Three traps in that table.

A published Reel reports media_type: VIDEO. If you store media_type to distinguish Reels from feed videos, your data is wrong. Request media_product_type instead. The same applies to Stories, which report IMAGE or VIDEO.

Carousels are n + 1 containers. Each child needs its own container created with is_carousel_item=true, then a parent container with media_type=CAROUSEL and children as a comma-separated list of up to 10 IDs. A 10-image carousel is 12 API calls. Every child inherits the 24-hour expiry independently - if child 1 was created 23 hours ago and child 10 five minutes ago, the parent will fail.

# child
curl -X POST "https://graph.instagram.com/v26.0/${IG_USER_ID}/media" \
  -d "image_url=https://cdn.example.com/1.jpg" \
  -d "is_carousel_item=true" \
  -d "access_token=${TOKEN}"

# parent
curl -X POST "https://graph.instagram.com/v26.0/${IG_USER_ID}/media" \
  -d "media_type=CAROUSEL" \
  -d "caption=Five things we shipped" \
  -d "children=17889455560051444,17889455560051445,17889455560051446" \
  -d "access_token=${TOKEN}"

Note also that carousel images are all cropped to match the first image, defaulting to 1:1. Customers notice this immediately, and it is not a bug you can fix in code - surface it in your UI before upload.

Stories are the constrained one. No caption. Metrics available for 24 hours only. And per Meta's own limitation note, Stories publishing is available to business accounts rather than all professional accounts - worth an eligibility check in onboarding rather than a failed post later.

Caption limits across all types: 2,200 characters, 30 hashtags, 20 @-mentions. Exceeding the caption length returns 36004 / 2207010; exceeding the tag count returns 100 / 2207040.

Rate limits and the publishing cap

Two independent ceilings. Hitting either one stops publishing, and they behave nothing alike.

Call volume: derived from impressions

Calls within 24 hours = 4800 × Number of Impressions

"Number of Impressions" is how many times content from that account entered someone's screen in the last 24 hours. The window is rolling, and the counter is scoped to the app–user pair, not to your app globally.

The consequence is the one that catches every integration: a brand-new client account has almost no call budget. Precisely when onboarding wants to backfill a content calendar, the ceiling is at its lowest. There is no documented floor for Instagram (Threads, by contrast, treats impressions as a minimum of 10).

Business Discovery and Hashtag Search are the exceptions - they fall under Platform rate limits (200 × users per hour) rather than Business Use Case limits. Messaging endpoints have their own per-second caps entirely.

Usage comes back in X-Business-Use-Case-Usage, keyed by business object ID, with type: "instagram":

x-business-use-case-usage: {
  "17841400008460056": [
    { "type": "instagram", "call_count": 34, "total_cputime": 12,
      "total_time": 15, "estimated_time_to_regain_access": 0 }
  ]
}

All three usage numbers are percentages, 0–100, not counts. estimated_time_to_regain_access is in minutes and is the single most useful field for a scheduler - it tells you exactly how long to park the queue for that account instead of guessing. The cross-platform version of this table is in social media API rate limits.

The publishing cap: query it, do not hardcode it

There is a separate cap on how many containers an account can publish per 24 hours, enforced on media_publish. Carousels count as one post.

Meta's own documentation gives three different numbers for it. As of 31 July 2026:

SourceFigure
Content Publishing guide, "Rate Limit" section100 posts / 24 h
Content Publishing guide, carousel "Limitations" section50 posts / 24 h
content_publishing_limit endpoint reference, quota_total50

Third-party guides confidently quote 25. Others quote 50. They are all quoting a snapshot of a number that has changed more than once and that Meta cannot keep consistent across two sections of the same page.

Meta ships an endpoint specifically so you do not have to guess:

curl -G "https://graph.instagram.com/v26.0/${IG_USER_ID}/content_publishing_limit" \
  -d "fields=config,quota_usage" \
  -d "access_token=${TOKEN}"
{
  "data": [
    {
      "quota_usage": 2,
      "config": { "quota_total": 50, "quota_duration": 86400 }
    }
  ]
}

quota_usage is containers published since the since timestamp (defaults to the last 24 hours; since cannot be older than that). quota_total and quota_duration are the live values for that account. Read them. Do not compile them into your code.

Exceeding the cap returns error 9 / 2207042, "You reached maximum number of posts."

Queueing under the cap

The cap is per account and the window is rolling, which rules out a simple daily counter.

  • Check before you publish, not after. One content_publishing_limit call per account per publish cycle is cheap insurance against a post that fails after you have already created and transcoded a container.
  • Reserve capacity, do not just count. In a system with concurrent workers, two jobs can each read quota_usage: 49 and both publish. Take a token from a per-account semaphore before creating the container, and release it on terminal failure.
  • Treat the window as rolling. With a 24-hour rolling window, a burst of 50 posts at 09:00 does not free capacity at midnight - it frees it at 09:00 the next day, gradually.
  • Degrade to "delayed", not "failed". A post blocked by the cap should be re-queued with the delay implied by the oldest publish in the window, and the customer should be told it is queued, not that it failed.
  • Fail fast on the terminal codes. Cap and throttle errors are retryable; a bad caption is not. Retrying a 36004 burns cap headroom to produce the same error.

Everything below is already handled across 227,000 connected accounts. You can build it yourself - or connect it.

bundle.social

The whole Graph surface, already integrated and audited.

Containers, polling and carousels behind a single call.

What changes at scale: notes from 227,000 connected accounts

Everything above describes one account. Multi-tenant is not that, repeated. Four properties of the API change character entirely once the fleet is large, and none of them are documented as such.

1. Rate limits are per account, so your global counter is meaningless

rates

The Instagram BUC limit is scoped to the app–user pair. We run about 227,000 connected Instagram accounts, which means 227,000 independent budgets, each derived from a different impressions figure, each moving hourly.

There is no aggregate view. There is no endpoint that returns "usage across all connected accounts." Your rate limiter's key must be (account_id), its state must be persisted, and it must be reconciled from response headers rather than computed.

And here is the wall: X-Business-Use-Case-Usage returns at most 32 objects in a single response. At our fleet size, enumerating budget state would take over 7,000 calls - to produce a snapshot that is stale before it finishes. So you do not enumerate. Budget state is built up incrementally, one response at a time, per account, and is always slightly stale for accounts you have not touched recently. Design for stale budgets rather than pretending you have a live global view.

The corollary: your most idle accounts have the least reliable budget estimates, and they are also the ones most likely to be brand-new with near-zero impressions. First publish for a new client is the highest-risk publish you will make.

2. Token lifecycle becomes a scheduled system, not an error handler

At one account, token refresh is something you do when a call fails. At six figures, it is a continuously running system with its own failure modes.

The mechanics force this. Instagram Login tokens live 60 days, must be at least 24 hours old to refresh, and cannot be recovered once expired - the customer has to re-authorize through a consent screen. That makes an unrefreshed token a support ticket, not a retry.

What the job actually needs:

  • A refresh horizon, not an expiry check. Refresh at day 45–50, not day 59. A refresh that fails on day 59 leaves no room for a second attempt.
  • Staggering. Accounts onboarded together during a migration all expire in the same week. Jitter the refresh schedule so the load - and the failure blast radius - spreads.
  • Two state machines, not one. If you support both login types you have Instagram User tokens (explicit refresh endpoint) and Facebook Page tokens (derived, revoked by Business Manager role changes). They fail differently and recover differently. One tokens table with a flavour column and one refresh worker per flavour.
  • A re-authorization path that is a product feature. Expired and revoked tokens are permanent until the user acts. That needs an email, an in-app banner, and a one-click reconnect - not a log line.

Here is what that looks like at our scale. Across roughly 227,000 connected Instagram accounts, a 60-day token life and the day-45–50 refresh horizon above work out to about 4,500 token refreshes every day just to keep the fleet alive. That is the steady-state floor, not a peak - it is the cost of doing nothing new.

The number that actually drives product decisions is the other one: about 10% of accounts per year need the user to re-authorize, because the token expired unrefreshed, the password changed, the account was checkpointed, or a Business Manager role was removed. At our fleet size that is roughly 22,000 re-consents a year - around 60 a day, every day.

Sixty a day is not an error queue. It is a recurring product surface, and it is why the last bullet above is not optional. If re-authorization is a log line and a support ticket, ten percent of your customers hit a dead integration each year and find out from their own audience. If it is an email, an in-app banner and a one-click reconnect, it is background noise. The engineering is identical; only the framing differs.

3. Fault isolation, or one bad account takes down the queue

instagram worker pool

A single global FIFO worker pool is the default design and the wrong one.

The failure looks like this. One customer's account is checkpointed (25 / 2207050, "The Instagram account is restricted"). Every job for that account fails, retries, backs off, and retries again. Those retries occupy workers. Meanwhile every healthy account waits behind them, and your on-call sees "publishing is slow" rather than "one account is broken."

Three mitigations, in order of importance:

Partition the queue by account. Round-robin across accounts rather than draining a global FIFO. One customer backfilling 500 posts must not starve everyone else - and one customer failing 500 posts must not either.

Circuit-break per account. After n consecutive terminal failures on an account, stop scheduling work for it and mark it degraded in your UI. A restricted account, a revoked token, and a deleted Instagram profile all produce infinite retry loops otherwise.

Classify errors before retrying, always. This matters more at scale than anywhere else, because retry storms are the mechanism by which one broken account consumes an entire worker pool.

4. Onboarding is the load spike, not steady state

Steady-state publishing, even across a very large fleet, is a trickle - spread thin across accounts and across the day. The spike is onboarding.

When an agency connects 40 client accounts on a Monday morning, you get, per account: an OAuth exchange, a long-lived token exchange, an account-info fetch, a media backfill, and often a batch of scheduled posts. All at once, against accounts whose impressions-derived budgets are the smallest they will ever be.

Rate-limit your own onboarding pipeline. Backfill lazily and in the background. Do not fetch 90 days of media insights synchronously during connect. The single most common self-inflicted outage in a multi-tenant social integration is an onboarding flow that behaves like a load test.

We have written up the data model, team isolation, and webhook fan-out for this in multi-tenant social media API architecture.

Error codes and retry classification

Instagram returns Graph API-shaped errors, but with an Instagram-specific subcode range that carries the real information. The code is generic; the error_subcode tells you what actually happened.

{
  "error": {
    "message": "The media could not be fetched from this uri",
    "type": "OAuthException",
    "code": 9004,
    "error_subcode": 2207052,
    "fbtrace_id": "EJplcsCHuLu"
  }
}

Log fbtrace_id on every failure. It is the only identifier Meta support can act on.

Code / subcodeMessageClassAction
-2 / 2207003Takes too long to download the mediaTransientRetry; check media host latency
-1 / 2207032Create media fail, please re-createTransientRecreate the container
-2 / 2207020The media you are trying to access has expiredTerminalContainer expired; recreate from source
4 / 2207051Restricted activity - flagged as spamTerminalStop. Do not retry. Review posting patterns
9 / 2207042You reached maximum number of postsCapRe-queue; check content_publishing_limit
24 / 2207008Media builder does not exist or expiredTransientRetry in 30 s – 2 min
25 / 2207050The Instagram account is restrictedTerminalCircuit-break the account; notify the customer
100 / 2207028Won't work as a carouselTerminalCarousel needs 2–10 items
100 / 2207040Cannot use more than {n} tagsTerminalReduce @-mentions to 20
352 / 2207026Video format is not supportedTerminalRe-encode to MP4 or MOV
9004 / 2207052Media could not be fetched from this uriTerminalURL not publicly reachable
9007 / 2207027Media is not ready for publishingTransientPoll status_code until FINISHED
36000 / 2207004Image too large to downloadTerminalExceeds 8 MiB
36001 / 2207005Image format not supportedTerminalJPEG only
36003 / 2207009Aspect ratio not supportedTerminalMust fall between 4:5 and 1.91:1
36004 / 2207010Caption too longTerminalTrim to 2,200 characters
80002Instagram BUC rate limit reachedThrottleBack off; use estimated_time_to_regain_access
190Access token expired / revokedTerminalRefresh, or trigger re-authorization
2 (transient)Unexpected errorTransientCheck container status_code before retrying

Two rules that follow from this table.

Terminal means terminal. Roughly two thirds of the rows above will never succeed on retry. Retrying a 36003 five times produces five identical failures, five wasted calls against an impressions-derived budget, and a delayed error message to the customer. Map every subcode to a class at the point where you parse the error, not in the retry loop.

4 / 2207051 is special. It is a behavioural block, not a rate limit. Retrying deepens it. The only correct response is to stop publishing for that account and look at what the content had in common.

Permission errors trace back to what the user granted at consent time - the per-permission requirements and review process are covered in Facebook API permissions and app review.

The shorter path

Everything above is one platform. The container flow, the polling loop, the idempotency check, the 500 token refresh schedules, the per-account rate limiter, the subcode classification table - that is the Instagram integration, before you add TikTok, LinkedIn, YouTube, or X, each with its own version of all of it.

With bundle.social's Instagram API integration, a Reel is one call:

curl -X POST "https://api.bundle.social/api/v1/post" \
  -H "x-api-key: ${BUNDLE_API_KEY}" \
  -H "content-type: application/json" \
  -d '{
    "teamId": "team_123",
    "postDate": "2026-08-14T09:00:00Z",
    "socialAccountTypes": ["INSTAGRAM"],
    "data": {
      "INSTAGRAM": {
        "type": "REEL",
        "text": "Launch day.",
        "uploadIds": ["upload_abc"],
        "shareToFeed": true,
        "collaborators": ["acme_design"]
      }
    }
  }'
Native Instagram Graph APIbundle.social
Container flow + pollingYou build itManaged
Publish idempotencyYou discover it in productionHandled
Token refresh across accountsYour scheduled jobManaged
Per-account rate limitingYour limiter, your stateManaged
Publishing cap enforcementYou query and reserveManaged
App Review + Business VerificationYours to passOurs, already passed
Adding Facebook, TikTok, LinkedInThree more integrationsThree more strings in socialAccountTypes

Teams isolate customers, an org-level API key covers all of them, and the queueing and fairness described above is the default rather than something you build.

Instagram API without managing a fleet of tokens. Explore the Instagram API

FAQ

Can the Instagram Graph API post to a personal account? No. It only works with Instagram professional accounts - Business or Creator. The API explicitly cannot access consumer accounts, and there is no workaround. Users must convert their account first.

How many posts per day can the API publish? It depends on the account, and Meta's own documentation contradicts itself (100 in one section, 50 in another, 50 from the API). Query GET /{ig-user-id}/content_publishing_limit and read config.quota_total for the live value. Carousels count as one post.

Why did my media container disappear? Containers expire 24 hours after creation. Query GET /{container-id}?fields=status_code - EXPIRED means exactly that. Create containers at publish time, not at schedule time.

What is the difference between Instagram Login and Facebook Login for the API? Instagram Login uses graph.instagram.com, Instagram User tokens, and needs no Facebook Page. Facebook Login uses graph.facebook.com, Page tokens, requires a linked Page, and is the only option for hashtag search, product tagging, and resumable video upload.

Is the Instagram Graph API free? Yes, there is no per-call charge. The costs are rate limits derived from account impressions, App Review, Business Verification, and the engineering time to maintain the integration.

How do I avoid publishing a post twice after a timeout? Before retrying media_publish, read the container's status_code. PUBLISHED means the post is live and you simply never received the media ID. Meta can publish successfully and still return a 500.

Marcel Czuryszkiewicz
Written by

Marcel Czuryszkiewicz

Co-Founder

Marcel is one of the builders behind bundle.social, creating a social media API with a strong focus on developer experience, reliability, and real support. Before bundle.social, he worked at Samsung R&D, Reply AI, and Docplanner, building internal tools that helped companies grow. He brought that same practical, product-focused mindset to bundle.social, so you can trust that your project is in good hands.