Get started
Meta APIs

Auto-posting to Instagram and Threads: containers, limits and the 4:5 rule

How the Instagram and Threads APIs publish: two-step media containers, status polling, the 100 and 250 posts-a-day limits, image rules and token lifetimes.

Meta’s two-step publishing flow: create a media container, poll its status until FINISHED, then publish
Short answer To auto-post an image to Instagram or Threads, create a media container with a public image URL and the caption, wait until the container’s status is FINISHED, then call the publish endpoint with the container ID. Instagram allows 100 API-published posts per account in a rolling 24 hours and only takes JPEGs between 4:5 and 1.91:1. Threads allows 250 posts a day, 500 characters of text and at most 5 links per post. Facebook Pages skip the container step: one upload request publishes the photo.
Key takeaways
  • Instagram and Threads publish in two steps (container → publish). Facebook Pages publish in one.
  • Instagram downloads your image itself, so image_url must be reachable from the public internet.
  • A 2:3 Pinterest image is rejected by Instagram. Render a separate 4:5 version.
  • Page tokens obtained from a long-lived user token do not expire. Threads tokens last 60 days and must be refreshed.
  • Check content_publishing_limit before publishing instead of finding the limit through errors.

We publish to Facebook Pages, Instagram and Threads from the same product catalogue every day, and the three Meta APIs look alike only at first glance. This guide walks through the flow we run in production, the limits we design around, and the errors that show up in the logs, with the official documentation linked at the end.

How publishing works: the two-step container model

Instagram and Threads do not accept a post in a single request. You first create a media container, an unpublished object that holds the image and the caption. Meta downloads and processes the media in the background. When the container is ready you publish it, and only then does the post appear on the profile.

The container flow: create container, poll status, publish, read permalink 1. Create POST /{id}/media image_url + caption 2. Poll status GET /{container} until FINISHED 3. Publish POST /media_publish creation_id 4. Permalink GET /{media} fields=permalink Other statuses IN_PROGRESS: wait ERROR: read message EXPIRED: after 24 h Facebook Pages: one step POST /{page-id}/photos with the file and message. No container, no public URL needed. Returns post_id straight away.
The same pattern serves Instagram (graph.facebook.com) and Threads (graph.threads.net). Facebook Pages publish in one request.

The split exists because media processing takes time, especially for video, and because carousels are assembled from several child containers. For a single image it feels like overhead. Treat it as a small state machine and it stays simple.

Before you start: accounts, permissions and tokens

Instagram and Facebook Pages

The Instagram account must be a Business or Creator account linked to a Facebook Page. One Facebook Login covers both. The user grants these permissions, which is the set we request:

  • pages_show_list, pages_manage_posts, pages_read_engagement: list the user’s Pages and post to them.
  • instagram_basic, instagram_content_publish: read the linked Instagram account and publish to it.
  • business_management: needed when the Pages are owned through a Business portfolio.

Exchange the login code for a short-lived user token, exchange that for a long-lived user token, then call GET /me/accounts. Each Page in the response carries its own Page access token, and a Page token obtained from a long-lived user token does not expire. We store one account per Page and one per linked Instagram profile (instagram_business_account on the Page). Both post with the Page token.

Threads

Threads uses its own app ID and secret, its own login and the scopes threads_basic and threads_content_publish. The long-lived token lasts 60 days and can be refreshed once it is at least a day old:

GET https://graph.threads.net/refresh_access_token
    ?grant_type=th_refresh_token
    &access_token=LONG_LIVED_TOKEN

Our background worker refreshes every Threads token that has fewer than 50 days left. That way a token is renewed about every ten days and one failed refresh never takes an account offline.

Publishing an image to Instagram

This is the code path Cast Forge runs, trimmed to the essentials (Python with requests):

import time, requests

GRAPH = "https://graph.facebook.com/v23.0"

def publish_instagram(ig_user_id, page_token, image_url, caption):
    # 1. container: Instagram downloads image_url itself
    cid = requests.post(f"{GRAPH}/{ig_user_id}/media", data={
        "image_url": image_url, "caption": caption,
        "access_token": page_token}).json()["id"]

    # 2. wait for processing
    for _ in range(24):
        status = requests.get(f"{GRAPH}/{cid}", params={
            "fields": "status_code", "access_token": page_token}).json()["status_code"]
        if status == "FINISHED": break
        if status in ("ERROR", "EXPIRED"): raise RuntimeError(status)
        time.sleep(5)

    # 3. publish
    return requests.post(f"{GRAPH}/{ig_user_id}/media_publish", data={
        "creation_id": cid, "access_token": page_token}).json()["id"]

Three details matter in practice:

  • The image must be public. Instagram fetches image_url from Meta’s servers, so a URL on localhost, behind a login or on a private network fails. We serve post images from a dedicated public path over HTTPS and keep the rest of the app private.
  • JPEG only. Instagram’s publishing API supports JPEG and nothing else. Convert PNG and WebP before you create the container.
  • Polling. Meta’s guidance is to check once a minute for no more than five minutes. In our logs single JPEG images are ready within seconds, so we poll every 5 seconds for up to 2 minutes. For video, use Meta’s slower cadence.

The publish call returns a media ID, not a link. Request GET /{media-id}?fields=permalink to get the post URL shown to users.

Publishing an image to Threads

Threads mirrors the same two steps on its own host, graph.threads.net:

POST https://graph.threads.net/v1.0/me/threads
     media_type=IMAGE
     image_url=https://example.com/p/product_45.jpg
     text=Linen apron in sage green…  https://shop.example/apron
     access_token=THREADS_TOKEN
→ { "id": "CONTAINER_ID" }

GET  https://graph.threads.net/v1.0/CONTAINER_ID?fields=status
→ { "status": "FINISHED" }

POST https://graph.threads.net/v1.0/me/threads_publish
     creation_id=CONTAINER_ID
→ { "id": "THREADS_MEDIA_ID" }

Meta recommends waiting about 30 seconds on average before publishing. Note that the status field is called status on Threads and status_code on Instagram. Text is limited to 500 characters, and emoji count by their UTF-8 byte length. Since December 22, 2025, a post with more than 5 unique links fails at container creation with THREADS_API__LINK_LIMIT_EXCEEDED.

Facebook Pages: one request, no container

Facebook Pages accept the image file in the request itself, so the image does not need a public URL:

POST https://graph.facebook.com/v23.0/{page-id}/photos
     message=Linen apron in sage green…  https://shop.example/apron
     source=@product.jpg          # multipart upload
     access_token=PAGE_TOKEN
→ { "id": "PHOTO_ID", "post_id": "PAGEID_POSTID" }

Use post_id, not the photo ID, when you link to the post: https://www.facebook.com/{post_id}.

The limits that matter

LimitInstagramThreadsFacebook Page
Posts via API100 per 24 h, rolling250 per 24 hStandard Graph API rate limits
Image formatsJPEG onlyJPEG, PNGUploaded file
Aspect ratio4:5 to 1.91:1Up to 10:1Not restricted
Image size-≤ 8 MB, 320–1440 px wide-
Text2,200 characters500 characters, ≤ 5 linksLong-form
Container lifetime24 hours-No container
Clickable links in textNoYesYes

Instagram exposes your remaining quota directly, so you never need to guess:

GET /{ig-user-id}/content_publishing_limit?fields=quota_usage,config

A human-paced schedule stays far below these ceilings. We cap new accounts at one post a day and ramp up slowly (see posting at a human pace). The limits matter mostly when a backlog builds up after downtime: reschedule missed posts instead of flushing them all at once.

One caption, three networks

We write the copy once per product and adapt it per network instead of paying for three AI generations:

  • Instagram: title and description, cut to 2,200 characters. The product link is left out because links in Instagram captions are not clickable, and a bare URL looks like spam.
  • Threads: title, description and link, cut so that the whole post including the link fits in 500 characters. When there is not enough room, the title alone carries the post and the link always survives.
  • Facebook: title, description and link in full.

Errors you will meet

SymptomCauseFix
Error 36003, subcode 2207009Image aspect ratio outside 4:5–1.91:1Render a 4:5 copy (1080 × 1350) for Instagram
Container ERRORImage could not be downloaded or processedRead error_message on the container. Check the URL is public and is a JPEG
Container EXPIREDNot published within 24 hoursCreate a new container right before publishing
OAuthException code 190Token expired or revokedRefresh (Threads) or ask the user to reconnect
THREADS_API__LINK_LIMIT_EXCEEDEDMore than 5 unique linksKeep one link per post

Production checklist

  • Serve post images from a public HTTPS path. Keep everything else behind login.
  • Render a 4:5 JPEG for Instagram and Threads, and keep the 2:3 version for Pinterest.
  • Store the Page token per account. Refresh Threads tokens well before day 60.
  • Poll container status with a time limit. Treat ERROR and EXPIRED as final.
  • Read content_publishing_limit before each publish.
  • Save the permalink so users can open the live post from your dashboard.
  • Take the Meta app Live before going public: in development mode, posts are only visible to the app’s roles.

Frequently asked questions

How many posts can I publish to Instagram through the API per day?

100 API-published posts per account in a rolling 24-hour window, a carousel counting as one. Read the live usage from GET /{ig-user-id}/content_publishing_limit before each publish.

How many posts can a Threads profile publish through the API?

250 posts in a 24-hour period. Posts with more than 5 unique links are rejected when the container is created.

Why does Instagram need a public image URL?

Instagram downloads the image itself when you create the container, so image_url must be reachable from the internet. Facebook Pages also accept the file uploaded directly in the request.

Sources

Platform limits and specs were checked against these pages on October 26, 2026. They change; the linked docs are authoritative.

  1. Instagram API: content publishing
  2. Threads API: posts
  3. Instagram Graph API: error codes
  4. Facebook Pages API: posts