Instagram

Partly verified — the account model, the Page link that gates everything, and the two-step publish. Includes the link failure that cost five attempts and a day.

Instagram

⚠️ Partly verified. The account-and-linking half was walked end to end on 2026-07-28: one brand linked successfully, one brand failed five times, one was blocked upstream. The publishing half is still researched only, written from Meta's developer docs — nobody here has published through the API yet.

Instagram is the hardest of the free channels, and almost none of the difficulty is in the code. Publishing is two HTTP calls. Getting an account into a state where those two calls are allowed is where the days go.

The account model — read this before anything else

Facebook and Instagram have opposite identity models. Assuming they match is the root of most of the pain, and it is the thing that scales badly across brands.

FacebookInstagram
IdentityOne personal profile administers many PagesEach professional profile is its own account
EmailOne email covers every PageA unique email per account
Reusing an emailFineRejected — "Another account is using the same email."
Cost of a new brandCreate a Page, 2 minutesCreate an account, verify an email, convert to professional, link it

Five brands therefore means one Facebook identity and five Instagram accounts on five distinct email addresses. Decide the address scheme before you create the first account — retrofitting it means re-creating accounts. See Ownership & accounts for the recommended scheme.

The six gates

Linking is the gate everyone remembers, because it is the one that fights back. It is the first of six. Do not plan the work as if the link is the finish line.

#GateWhat it takes
1Instagram account exists on its own emailMobile app or web signup
2Account is professional (Business or Creator)Mobile app, one screen
3Account is linked to the brand's Facebook PageThe flow below — the hard one
4Page and account owned by the same business portfolioBusiness Settings
5App holds instagram_basic + instagram_content_publishUse-Case grant + token re-mint — possibly nothing more; see the hypothesis below
6INSTAGRAM_USER_ID_<BRAND> in Vercel, publish path wiredEnv var only — the code shipped 2026-07-30 (lib/instagram.ts, gated wired: false)

Gates 1–4 are per brand. Gates 5–6 are paid once for all brands.

Standard-Access hypothesis

Gate 5 has been written up as Advanced Access → App Review → Business Verification. That may be wrong, and the evidence is ours: on 2026-07-27 the same claim about read_insights and pages_read_user_content fell — both turned out to be Standard Access, granted the moment they were added to the app's Use Case and a token was re-minted. No review, no verification.

Meta's access-levels doctrine says Standard Access covers requesting permissions from users who have a role on the app — admins, developers, testers. kun posts to assets its own admin controls; that is the Standard case, and on paper it covers instagram_content_publish exactly as it covered the read scopes.

The ~10-minute test, via Mkan (the one brand whose gates 1–4 are done):

  1. App Dashboard → Use cases → Customize → add instagram_basic + instagram_content_publish (first + Add click may pop "Something went wrong" — click OK, click again).
  2. Re-mint the Page token (scopes are fixed at mint time — the runbook in Facebook applies; the password modal is Abdout's).
  3. POST /{ig-account-id}/media with a test image → POST /{ig-account-id}/media_publish against @mkan.sd (17841444144779316).

If it publishes, gate 5 was never a programme blocker and Instagram go-live is exactly this: paste the re-minted token into FACEBOOK_PAGE_ACCESS_TOKEN_MKAN, set INSTAGRAM_USER_ID_MKAN=17841444144779316, and flip the channel's wired flag in src/components/root/social/config.ts — the adapter, the transport, the image-required refusal, and the health row all shipped 2026-07-30 behind that flag. If Meta refuses with a permission error naming Advanced Access, the ownership chapter's verification path stands as written — and either way that chapter's work (naming, verification, second admin, per-brand emails) remains worth doing for durability.

What you get

Publish images, videos, Reels, carousels, and stories to an Instagram Business or Creator account via the Graph API Content Publishing endpoints. Personal accounts cannot be posted to programmatically, and there is no workaround.

Prerequisites

Instagram is not really a separate integration — it rides on Facebook's. Do Facebook first. You need its Page, its business portfolio, its Business-type app, and its Page token; Instagram adds no credential of its own.

Steps

1. Create the account on its own email

Instagram signup, with an address nobody else has used on Instagram. If the address is already attached to any Instagram account — including a dormant personal one — signup fails at the email step with "Another account is using the same email."

2. Switch it to professional

Instagram mobile appSettings → Account type and tools → Switch to professional accountBusiness. Pick a category; it is editable later.

Confirming afterwards that it really is professional is less obvious than it should be. The Professional dashboard button on the web profile only renders when the account's "Display category label" setting is on. Its absence is not evidence the account is personal — we chased that false signal for hours. Check the account type in the mobile app's settings, or trust the Business Settings asset pane.

3. Put the Facebook Page in a business portfolio

Business Settings → Accounts → Pages → Add → Add an existing Page. Both the Page and the Instagram account must end up owned by the same portfolio. See Ownership & accounts.

Three entry points exist and they are not equivalent. All three open the same Instagram OIDC consent screen, so they look identical from the outside; they do different things.

Entry pointWhereWhat it actually does
Page home → Connect InstagramBusiness Suite home headerThe intended flow. Worked for one brand, failed four times for another
Business Settings → Instagram accounts → Add → Claim an Instagram accountBusiness SettingsClaims the account as a portfolio asset. Useful and reliable — but it does not create a Page↔account link
Page asset → Connect assets → Instagram accountBusiness Settings → Pages → Page → Connect assetsSame intent as the first, different code path. Also failed

Do the claim first — it is the reliable one and it is a prerequisite for gate 4 anyway. Then do the link. Then verify, because this flow lies.

The consent screen self-closes identically whether the link succeeded or silently failed, and no error is ever shown. Do not trust it. Check the asset instead:

Business Settings → Accounts → Pages → <the Page> → Connected assets
  • Linked: "1 asset is connected to this Facebook Page", the Instagram username, and a Disconnect button.
  • Not linked: "No connected assets".

Two secondary tells, useful as a cross-check:

  • The Page home header flips from "Connect Instagram" to "Edit Instagram profile", and an Instagram followers counter appears next to Facebook followers.
  • The portfolio's People list gains Instagram-badged business users named after the account. A successful link created two; a failed link created none. This is the sharpest diagnostic we found — a link that produced no business user did not happen, whatever the UI implied.

The double-avatar cluster on the Page header — two overlapping circles, one Facebook-badged, one Instagram-badged — is not a tell. An unlinked Page shows it too.

6. Find the Instagram account id

curl -s "https://graph.facebook.com/v21.0/<PAGE_ID>\
?fields=instagram_business_account&access_token=<PAGE_TOKEN>"
# → {"instagram_business_account":{"id":"178414..."}}

The field is silently omitted if your token lacks instagram_basic — you get a 200 with the field simply missing. That looks exactly like "this Page has no Instagram account". Check your token's scopes before concluding anything from an absent field.

The id is also readable without any token: Business Settings → Instagram accounts → the account → ID.

7. Publish — two steps, always

# 1. create the container
curl -s -X POST "https://graph.facebook.com/v21.0/<IG_ACCOUNT_ID>/media" \
  -d "image_url=https://cdn.example.com/post.jpg" \
  -d "caption=Your caption here" \
  -d "access_token=<PAGE_TOKEN>"
# → {"id":"17895695668004550"}   ← container id, NOT a published post
 
# 2. publish it
curl -s -X POST "https://graph.facebook.com/v21.0/<IG_ACCOUNT_ID>/media_publish" \
  -d "creation_id=17895695668004550" \
  -d "access_token=<PAGE_TOKEN>"

The traps

The link fails silently. This is the expensive one. The OIDC consent tab shows "Log in as <handle>", accepts the click, and closes itself — the same behaviour as success. Nothing is created and no error surfaces anywhere. Five attempts across three entry points produced five identical "successes" and zero links. Always verify against Connected assets, and treat the consent screen as carrying no information at all.

Claiming is not linking. Claim an Instagram account puts the account in your portfolio. People read that as done. The Page is still unconnected, and every downstream check still fails.

Instagram web has no linking surface. Not in Professional account tools, not in Business tools and controls, not in Edit profile. All three were read in full. If the Business Suite side refuses, the mobile app (Profile → Edit profile → Page → Connect an existing Page) is the only remaining route — there is no third option to look for.

Business Suite's inline "Create a new Instagram profile" is broken. It returns "Unable to create an account… please create a new account on Instagram". Create the account on Instagram first, then come back and link it.

A Page in someone else's portfolio cannot be linked at all. Meta refuses with "you need full control of the … business portfolio that the Page belongs to". There is no per-Page override; you resolve the portfolio, or you move the Page.

Media must be a public URL. There is no upload endpoint. Instagram fetches the file from your URL, so it must be reachable from Meta's servers — a CDN, not localhost, not a signed URL that expires in 60 seconds, not anything behind auth. The most common code-level failure.

Two steps, not one. A container id is not a post. Code that stops after /media publishes nothing and looks successful.

Video containers are asynchronous. Poll status_code on the container until FINISHED before calling /media_publish.

A publishing limit applies — roughly 50 posts per 24 hours per account.

Carousels need a container per item, then a parent with media_type=CAROUSEL and the children's ids. Stories and Reels are separate media types, not flags on a normal post.

Environment variables

VariableExampleRequired
INSTAGRAM_ACCOUNT_ID_<PRODUCT>INSTAGRAM_ACCOUNT_ID_MKANYes
FACEBOOK_PAGE_ACCESS_TOKEN_<PRODUCT>reused from FacebookYes

No separate token — Instagram authenticates with the linked Page's token. Which also means a crossed Facebook token silently posts to the wrong brand's Instagram.

Verify

curl -s "https://graph.facebook.com/v21.0/<IG_ACCOUNT_ID>\
?fields=username,followers_count&access_token=<PAGE_TOKEN>"

Getting the username back confirms the link, the permissions, and the token together — it is the one check that exercises all three at once.

Limits

  • Rate: ~50 published posts per 24h per account
  • Media: images by public URL; video by public URL with async processing; carousels up to 10 items
  • Caption: 2,200 characters, up to 30 hashtags
  • Cost: free
  • AI disclosure: same posture as Facebook — no organic API flag; comply by policy for photorealistic synthetic media