Skip to content

Connect an Instagram account

The platform publishes approved drafts to an Instagram Business or Creator account through the Meta Graph API. It is the closest sibling of Connect a Facebook Page — same Meta app family, same Graph host, same stored credential — with three differences that shape everything below.

The connect flow runs the Facebook Login dance and then keeps going one hop:

  1. authorization code → short-lived user token
  2. short-lived → long-lived user token (~60 days) via fb_exchange_token
  3. long-lived user token → the Pages you administer, asking for each Page’s linked Instagram account in the same call (GET /me/accounts?fields=id,name,access_token,instagram_business_account{id,username})
  4. the chosen Page’s access token is stored, encrypted, on an owner-scoped post://<ig-username>@instagram.com channel

Only Pages that have a linked Instagram account are offered. A Page without one is not a slow path — it is a dead end, so it never appears in the picker. If you administer several qualifying Pages, the connect parks the long-lived user token in a signed httpOnly cookie and returns you to /channels?connect=choose&provider=instagram to pick; the chosen id is re-checked against Meta before anything is stored.

The channel is named after the Instagram handle, not the Page — that is the thing you chose.

Instagram is the one destination in the platform where publishing is two calls:

POST /{ig-user-id}/media image_url + caption → a container id
POST /{ig-user-id}/media_publish creation_id=<that id> → the published media id

There is no single-call form. A container that is created and never published costs nothing and expires on Meta’s side, so a failure between the two steps is safe: the card goes to failed with the error and nothing is live.

The post’s URL is then read back (GET /{media-id}?fields=permalink) rather than derived — an Instagram permalink is built from a shortcode and the media id is numeric. That read is best-effort: the post is already live by then, so a failure leaves the card without a link rather than marking a published row failed.

Meta fetches image_url from its own servers. Use an asset on the platform’s public lane (GET /api/assets/:org/:visibility/:key with a public visibility), or any other anonymously-readable https URL. A private asset produces a container error naming the fetch, which the platform surfaces verbatim on the card — that message is the useful one, so it is not summarised away.

A video is a different container (media_type=REELS, video_url, plus a status-polling loop before publish), and the publisher does not build one. The destination declares video: none, so a video-carrying draft is refused when you write it, not when it publishes.

You can reuse the Meta app behind your Facebook Page connection, or use a separate one. Either way the app needs the Instagram permissions below, and a redirect URL registered for this provider:

https://platform-api.viite.ai/api/connect/instagram/callback
PermissionWhy
pages_show_listenumerate the Pages you administer
pages_read_engagementread Page data — Meta’s declared dependency, and what reads instagram_business_account
instagram_basicread the linked account’s id + username
instagram_content_publishcreate the container and publish it
instagram_manage_commentspost the first comment
KeyClassNotes
INSTAGRAM_CLIENT_IDpublicThe Meta App ID.
INSTAGRAM_CLIENT_SECRET🔒 secretNever goes in allowed.config.types.ts.
INSTAGRAM_OAUTH_REDIRECT_URIpublicMust byte-for-byte match a URL registered on the app.
INSTAGRAM_LOGIN_CONFIG_IDpublic, optionalFacebook Login for Business configuration id. When set, the authorize call sends config_id and no scope.

Instagram stays dormant until all three required keys are set (plus SOCIAL_TOKEN_ENCRYPTION_KEY), exactly like X, LinkedIn and Facebook. A partial set logs a warning at boot and registers nothing.

GET /api/connect/instagram/introspect asks Meta, via GET /debug_token, which scopes are really on the stored token.

This matters more here than elsewhere. With INSTAGRAM_LOGIN_CONFIG_ID set, the authorize call deliberately sends config_id and no scope, which hands the permission list to a configuration living in the App Dashboard that Meta exposes through no Graph endpoint. A missing instagram_content_publish is therefore invisible: the connect succeeds, the channel row looks complete, and the first symptom is a publish failing. Introspection is the only way to ask.

instagram_content_publish and its siblings sit at Standard Access until Meta grants Advanced Access (App Review + Business Verification, app in Live mode). Standard Access works only for users who hold a role on the app — the same gate that constrains the Facebook Page publisher, and the same one that decides when this becomes usable by arbitrary customers. The code ships before the grant; the grant is not a code change.

What you seeWhat it means
No Facebook Pages found for this accountYou administer no Page. An Instagram account publishes through a Page, so there is nothing to connect yet.
None of the N Page(s) you administer has a linked Instagram Business or Creator accountYou have Pages, but no Instagram account linked to one. Link it in the Page’s settings, then reconnect.
Instagram posts must carry an imageThe draft was armed without one. Add an image in the post editor.
The image could not be fetched from the URL provided (verbatim from Meta)image_url is not publicly reachable — usually a private asset.
The connected Facebook Page no longer has a linked Instagram Business or Creator accountThe link was removed on Meta’s side after connecting. Re-link and reconnect the channel.
Instagram page tokens cannot be refreshed automaticallyMeta issues no refresh token for this flow. Reconnect.