Skip to main content
Before you start. Instagram requires a Business or Creator account. Personal accounts cannot publish via the Content Publishing API regardless of scopes granted — the connect flow rejects them with instagram.account_type.requires_professional and a remediation pointing at the Switch to professional account toggle in the IG app.

Quick reference

Limit / capabilityValue
Caption character limit2,200 graphemes
Items per carousel2 – 10 (mixed image + video allowed)
Image formatJPEG only (PNG, WebP, HEIC, GIF rejected by Meta)
Image size8,000,000 bytes (8 MB)
Video formatsmp4, mov
Video size1,000,000,000 bytes (1 GB)
Reels duration≤ 90 s (platform-enforced)
Alt text per item2,200 graphemes
Post types supportedsingle image, single video (Reels), carousel
Text-only postNot supported — every post requires media
SchedulingYes (via scheduledAt)
Reply / thread supportNot supported in v1
First commentNot supported in v1
StoryNot supported in v1
Inbox / DMNot supported in v1
AnalyticsNot supported in v1

Connect an account

Instagram has its own OAuth (since the 2024 Instagram Login product) — distinct from Facebook Login. There is no FB fan-out anymore; connecting Instagram does not also connect Facebook. POST /v1/accounts/connect/instagram produces an authorize URL at instagram.com/oauth/authorize. After the user grants, the callback runs three Graph calls:
  1. Exchange code → short-lived token (1 h).
  2. Swap short-lived → long-lived (60 d).
  3. GET /me?fields=id,user_id,username,account_type — the Content Publishing API expects id (the IG-scoped user id), not user_id — the provider persists id as platformAccountId. If account_type === "PERSONAL" the connect is rejected.

Scopes

instagram_business_basic           — read /me + identity
instagram_business_content_publish — create + publish containers
extended scopes (off by default) cover comment + insight reads — not required for publishing.

Token lifecycle

Long-lived tokens last 60 days and are refreshable any time after the first 24 hours via GET /refresh_access_token. The provider refreshes on schedule; token.expiring fires before expiry.

Post types

Single image

single-image.json
{
  "targets": [{ "accountId": "..." }],
  "text": "Caption is optional",
  "media": [
    { "kind": "image", "mediaId": "med_…", "altText": "..." }
  ]
}

Single video (Reels)

Single-video posts route through the REELS container — Instagram retired the legacy VIDEO product surface in 2024.
reels.json
{
  "targets": [{ "accountId": "..." }],
  "text": "Reels caption",
  "media": [
    { "kind": "video", "mediaId": "med_…" }
  ]
}
Carousel children can mix image and video items.
carousel.json
{
  "targets": [{ "accountId": "..." }],
  "text": "Mixed carousel of three",
  "media": [
    { "kind": "image", "mediaId": "med_a…" },
    { "kind": "video", "mediaId": "med_b…" },
    { "kind": "image", "mediaId": "med_c…" }
  ]
}
The publisher creates each child container (is_carousel_item=true), polls each to FINISHED, then creates a CAROUSEL parent with the child ids and publishes the parent.

Wisdom (platform-specific things that bite)

  • JPEG only for images. PNG, WebP, HEIC, and GIF will be rejected with code 100, error_subcode 2207003. Preflight catches this locally so you don’t lose a roundtrip — re-encode to JPEG via your image pipeline (or upload via POST /v1/media which preserves the source mime).
  • Media URLs must be publicly reachable by Meta’s CDN — anonymous HTTPS, no auth, no Drive links, no signed S3 URLs that expire before the container finishes. Unreachable URLs surface as OAuthException 2207052, the canonical opaque-rejection.
  • The first 125 characters of the caption show before the “more” fold in feed. Front-load anything important.
  • Container expiry: Instagram’s media containers live for 24 hours after creation. The publisher creates + publishes in one synchronous flow so this rarely matters, but a scheduled post that ages out triggers instagram.container.expired.
  • The Content Publishing API uses id (the IG-scoped user id) on /{ig-user-id}/medianot user_id from the token-exchange response. The provider stores the right one; if you ever rebuild this manually, sending user_id returns code:2.
  • Reels duration is platform-enforced. We size-check at preflight, but the 90 s ceiling is checked by Instagram at container-finalize and surfaces as a platform rejection.
  • The publisher’s response carries a permalink fetched best-effort via GET /{media-id}?fields=permalink. If that call 404s, the publish still succeeds and the response carries the id alone.

Common errors

Error ruleWhat it meansHow to fix
instagram.media.requiredNo media on the postAttach at least one image or video; Instagram has no text-only feed surface.
instagram.text.max_graphemesCaption > 2,200 graphemesTrim under 2,200.
instagram.media.count_maxMore than 10 items in a carouselReduce to ≤10, or split into multiple posts.
instagram.media.mime_allowedImage isn’t JPEG, or video isn’t mp4/movRe-encode to JPEG (images) or mp4/mov (video).
instagram.media.image_size_maxImage > 8 MBCompress under 8,000,000 bytes.
instagram.media.video_size_maxVideo > 1 GBCompress under 1,000,000,000 bytes.
instagram.container.expiredContainer aged past Instagram’s 24 h windowRe-create and publish in one flow.
instagram.account_type.requires_professionalConnect attempted with a PERSONAL accountSwitch the IG account to Business or Creator in-app, then reconnect.

What you can’t do (yet)

  • Stories (Stories use a separate STORIES container type not in v1).
  • Direct messages.
  • First comment as the author.
  • Tagging users or product tags.
  • Editing or deleting a published post.
  • Posting from a Personal Instagram account (Meta’s API restriction — switch to Business or Creator).
  • Reading comments, replies, or insights (requires extended scopes plus read endpoints not in v1).

API reference