⚠️ 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.
| Identity | One personal profile administers many Pages | Each professional profile is its own account |
| One email covers every Page | A unique email per account | |
| Reusing an email | Fine | Rejected — "Another account is using the same email." |
| Cost of a new brand | Create a Page, 2 minutes | Create 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.
| # | Gate | What it takes |
|---|---|---|
| 1 | Instagram account exists on its own email | Mobile app or web signup |
| 2 | Account is professional (Business or Creator) | Mobile app, one screen |
| 3 | Account is linked to the brand's Facebook Page | The flow below — the hard one |
| 4 | Page and account owned by the same business portfolio | Business Settings |
| 5 | App holds instagram_basic + instagram_content_publish | Use-Case grant + token re-mint — possibly nothing more; see the hypothesis below |
| 6 | INSTAGRAM_USER_ID_<BRAND> in Vercel, publish path wired | Env 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):
- App Dashboard → Use cases → Customize → add
instagram_basic+instagram_content_publish(first+ Addclick may pop "Something went wrong" — click OK, click again). - Re-mint the Page token (scopes are fixed at mint time — the runbook in Facebook applies; the password modal is Abdout's).
POST /{ig-account-id}/mediawith a test image →POST /{ig-account-id}/media_publishagainst@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 app → Settings → Account type and tools → Switch to professional account → Business. 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.
4. Link the account to the Page
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 point | Where | What it actually does |
|---|---|---|
| Page home → Connect Instagram | Business Suite home header | The intended flow. Worked for one brand, failed four times for another |
| Business Settings → Instagram accounts → Add → Claim an Instagram account | Business Settings | Claims the account as a portfolio asset. Useful and reliable — but it does not create a Page↔account link |
| Page asset → Connect assets → Instagram account | Business Settings → Pages → Page → Connect assets | Same 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.
5. Verify the link — the only trustworthy signal
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
| Variable | Example | Required |
|---|---|---|
INSTAGRAM_ACCOUNT_ID_<PRODUCT> | INSTAGRAM_ACCOUNT_ID_MKAN | Yes |
FACEBOOK_PAGE_ACCESS_TOKEN_<PRODUCT> | reused from Facebook | Yes |
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
On This Page
InstagramThe account model — read this before anything elseThe six gatesStandard-Access hypothesisWhat you getPrerequisitesSteps1. Create the account on its own email2. Switch it to professional3. Put the Facebook Page in a business portfolio4. Link the account to the Page5. Verify the link — the only trustworthy signal6. Find the Instagram account id7. Publish — two steps, alwaysThe trapsEnvironment variablesVerifyLimits