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.
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_codeto 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.
texton Threads,captionon Instagram - Base URL.
https://graph.threads.net/v1.0/vshttps://graph.facebook.com/v22.0/ - Endpoints.
/threadsand/threads_publishvs/mediaand/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
imagesfield 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, andAuthorizationheaders - Images are uploaded as binary — LinkedIn does not pull from URLs like Instagram and Threads
- The caption field is
commentary, notcaptionortext
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
| Threads | |||
|---|---|---|---|
| Model | Container (parent-child) | Container (parent-child) | Flat array in post body |
| Item creation | POST /{user}/media | POST /{user}/threads | PUT binary to upload URL |
| Parent/post creation | POST /{user}/media | POST /{user}/threads | POST /rest/posts |
| Publish step | POST /{user}/media_publish | POST /{user}/threads_publish | None (post creation publishes) |
| Children format | Comma-separated string | Comma-separated string | JSON array of objects |
| Caption field | caption | text | commentary |
| Item limit | 1–10 | 2–20 | 2–20 |
| Media source | Platform pulls from URL | Platform pulls from URL | You upload binary directly |
| Timing constraint | None | 30-second minimum wait | None |
| Mix image and video | Yes | Yes | No |
| 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:
-
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.
-
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. -
Wait for processing. Instagram containers need polling until
FINISHED. Threads containers need at least 30 seconds before you can proceed. LinkedIn images need polling untilAVAILABLE. Three different readiness checks. -
Assemble the post. Instagram gets a parent container with
childrenas a comma-separated string andcaption. Threads gets the same shape but withtextand a different endpoint. LinkedIn gets amultiImagecontent block with a JSON array andcommentary. -
Publish. Instagram and Threads each need a separate publish call. LinkedIn is already published from step 4.
-
Map the caption. The same text goes to
captionon Instagram,texton Threads, andcommentaryon 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.
The same carousel, through Postproxy
Here is what the same 5-image carousel looks like through Postproxy:
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
multiImagepost assembly for LinkedIn - Per-platform processing status polling and readiness checks
- The 30-second timing constraint on Threads
- Caption field mapping across
caption,text, andcommentary - 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.