Publishing carousel posts across Instagram, LinkedIn, and Threads via API

Each platform implements multi-image posts differently — containers, arrays, item IDs. Walk through the mechanics of each and how to normalize them in a single workflow.

Publishing carousel posts across Instagram, LinkedIn, and Threads via API

Three platforms, three models

You have five images. You want them posted as a carousel on Instagram, LinkedIn, and Threads. It should be three API calls — one per platform. It is not.

Instagram and Threads both use a container model, but with different base URLs, different item limits, and different timing constraints. LinkedIn skips containers entirely and uses a flat array of image URNs inside a multiImage content wrapper. The three workflows share almost no code.

This guide walks through the mechanics of each platform’s carousel implementation, what diverges, and what it takes to unify them.

Instagram: containers, children, publish

Instagram carousels are a three-step process. You create individual item containers, bundle them into a parent container, and publish the parent.

Step 1 — Create item containers. For each image, POST to /{ig-user-id}/media with is_carousel_item set to true:

{
"image_url": "https://example.com/photo1.jpg",
"is_carousel_item": true
}

Each call returns a container ID. You need one per image, up to 10 items per carousel.

Step 2 — Create the carousel container. POST to /{ig-user-id}/media with the children IDs:

{
"media_type": "CAROUSEL",
"children": "CONTAINER_1_ID,CONTAINER_2_ID,CONTAINER_3_ID",
"caption": "Your carousel caption"
}

The children parameter is a comma-separated string of container IDs — not a JSON array. This returns the parent container ID.

Step 3 — Publish. POST to /{ig-user-id}/media_publish with the parent container ID:

{
"creation_id": "PARENT_CONTAINER_ID"
}

Key details:

  • All carousel images are cropped to the aspect ratio of the first image
  • Instagram pulls media from your URLs — the images must be publicly accessible
  • You should poll /{container-id}?fields=status_code to confirm each container finishes processing before creating the parent. Statuses: IN_PROGRESS, FINISHED, ERROR
  • Carousels can mix images and videos

The container model means you cannot create a carousel in a single request. The minimum is N+2 API calls: one per item, one for the parent, one to publish.

Full details in the Instagram publishing guide.

Threads: same model, different constraints

Threads uses the same container-based approach as Instagram. The API surface looks almost identical — create item containers, create a carousel container, publish. But the details diverge.

Step 1 — Create item containers. POST to /{threads-user-id}/threads with is_carousel_item set to true:

{
"media_type": "IMAGE",
"image_url": "https://example.com/photo1.jpg",
"is_carousel_item": true
}

Note the differences from Instagram: the endpoint is /threads instead of /media, the base URL is graph.threads.net instead of graph.facebook.com, and you must specify media_type explicitly on item containers.

Step 2 — Create the carousel container. POST to /{threads-user-id}/threads:

{
"media_type": "CAROUSEL",
"children": "CONTAINER_1_ID,CONTAINER_2_ID,CONTAINER_3_ID",
"text": "Your carousel caption"
}

The caption field is text, not caption like Instagram.

Step 3 — Publish. POST to /{threads-user-id}/threads_publish:

{
"creation_id": "CAROUSEL_CONTAINER_ID"
}

Where Threads diverges:

  • Mandatory wait. You must wait at least 30 seconds between creating a container and publishing it. Publish too early and the request fails. This is unique to Threads
  • Item count. Threads supports 2–20 items per carousel (minimum 2). Instagram supports 1–10
  • Caption field. text on Threads, caption on Instagram
  • Base URL. https://graph.threads.net/v1.0/ vs https://graph.facebook.com/v22.0/
  • Endpoints. /threads and /threads_publish vs /media and /media_publish

The container pattern is the same. Almost nothing else is.

Full details in the Threads publishing guide.

LinkedIn: no containers, flat array

LinkedIn does not use a container model. Multi-image posts are created in two steps: upload each image, then create the post with all image references at once.

Step 1 — Upload images. For each image, initialize an upload by POSTing to /rest/images?action=initializeUpload:

{
"initializeUploadRequest": {
"owner": "urn:li:person:YOUR_PERSON_URN"
}
}

This returns an uploadUrl and an image URN. PUT the binary image data to the uploadUrl. Repeat for each image.

Step 2 — Create the post. POST to /rest/posts with a multiImage content block:

{
"author": "urn:li:person:YOUR_PERSON_URN",
"commentary": "Your carousel caption",
"visibility": "PUBLIC",
"distribution": {
"feedDistribution": "MAIN_FEED"
},
"content": {
"multiImage": {
"images": [
{ "id": "urn:li:image:IMAGE_1_URN", "altText": "First image" },
{ "id": "urn:li:image:IMAGE_2_URN", "altText": "Second image" },
{ "id": "urn:li:image:IMAGE_3_URN", "altText": "Third image" }
]
}
}
}

There is no publish step. The POST to /rest/posts creates and publishes the post in one call.

Key details:

  • The images field is a JSON array of objects, not a comma-separated string of IDs
  • Each image object can include altText (up to 4,086 characters)
  • LinkedIn requires 2–20 images for a multi-image post
  • Every request needs Linkedin-Version, X-Restli-Protocol-Version: 2.0.0, and Authorization headers
  • Images are uploaded as binary — LinkedIn does not pull from URLs like Instagram and Threads
  • The caption field is commentary, not caption or text

LinkedIn’s model is flatter but not simpler. The image upload requires polling /rest/images/{urn} until the status is AVAILABLE before referencing it in the post. And the REST protocol headers are required on every single request.

Full details in the LinkedIn publishing guide.

Side by side

InstagramThreadsLinkedIn
ModelContainer (parent-child)Container (parent-child)Flat array in post body
Item creationPOST /{user}/mediaPOST /{user}/threadsPUT binary to upload URL
Parent/post creationPOST /{user}/mediaPOST /{user}/threadsPOST /rest/posts
Publish stepPOST /{user}/media_publishPOST /{user}/threads_publishNone (post creation publishes)
Children formatComma-separated stringComma-separated stringJSON array of objects
Caption fieldcaptiontextcommentary
Item limit1–102–202–20
Media sourcePlatform pulls from URLPlatform pulls from URLYou upload binary directly
Timing constraintNone30-second minimum waitNone
Mix image and videoYesYesNo
Min API calls (5 images)7 (5 items + parent + publish)7 (5 items + parent + publish)6 (5 uploads + post)

What normalization looks like

To publish a carousel to all three platforms from a single input, your system needs to:

  1. Resolve media. Instagram and Threads need publicly accessible URLs. LinkedIn needs binary uploads. If your source is a URL, you upload it to LinkedIn. If your source is a file, you host it for Instagram and Threads.

  2. Create items per platform. Instagram and Threads each need individual container creation calls with is_carousel_item: true. LinkedIn needs individual image upload and initialization calls. Three separate loops, three different request shapes.

  3. Wait for processing. Instagram containers need polling until FINISHED. Threads containers need at least 30 seconds before you can proceed. LinkedIn images need polling until AVAILABLE. Three different readiness checks.

  4. Assemble the post. Instagram gets a parent container with children as a comma-separated string and caption. Threads gets the same shape but with text and a different endpoint. LinkedIn gets a multiImage content block with a JSON array and commentary.

  5. Publish. Instagram and Threads each need a separate publish call. LinkedIn is already published from step 4.

  6. Map the caption. The same text goes to caption on Instagram, text on Threads, and commentary on LinkedIn. Three field names for the same concept.

None of these steps are technically difficult. But each one branches three ways, and each branch has its own error modes, status codes, and edge cases. The complexity is not in any single platform — it is in maintaining all three paths and keeping them in sync.

Here is what the same 5-image carousel looks like through Postproxy:

Terminal window
curl -X POST "https://api.postproxy.dev/api/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"post": {
"body": "5 lessons from our first year building in public"
},
"profiles": ["instagram", "linkedin", "threads"],
"media": [
"https://example.com/slide1.jpg",
"https://example.com/slide2.jpg",
"https://example.com/slide3.jpg",
"https://example.com/slide4.jpg",
"https://example.com/slide5.jpg"
]
}'

One request. Five media URLs. Postproxy handles the container creation on Instagram and Threads, the binary uploads to LinkedIn, the processing waits, the field name mapping, and the per-platform publish calls.

What Postproxy handles

Postproxy normalizes carousel publishing so your system does not have to maintain three separate workflows:

  • Container creation and URL-based media pull for Instagram and Threads
  • Binary image upload and multiImage post assembly for LinkedIn
  • Per-platform processing status polling and readiness checks
  • The 30-second timing constraint on Threads
  • Caption field mapping across caption, text, and commentary
  • Per-platform item count validation (10 on Instagram, 20 on Threads and LinkedIn)
  • Per-platform outcome reporting — what published, what failed, and why

Your system provides a list of image URLs and a caption. Postproxy handles the rest.

Connect your accounts and start publishing carousels through the Postproxy API.

Ready to get started?

Start with our free plan and scale as your needs grow. No credit card required.