---
title: "Bulk schedule up to 100 posts"
method: POST
path: "/social-posts/bulk"
tags: ["Posts"]
---

# Bulk schedule up to 100 posts

`POST /social-posts/bulk`

## Request

- Base URL: `https://post.adaptlypost.com/post/api/v1`
- URL: `https://post.adaptlypost.com/post/api/v1/social-posts/bulk`
- Auth: HTTP bearer

Requires the `posts.schedule` permission.

## Headers

- `X-Workspace-Id` string

## Request body

- BulkSchedulePostsRequest
  - `platforms` PlatformType[], required: Platforms to post to. Each one also needs its connection ids. TELEGRAM has no connection ids in the API, so Telegram posts can only be created in the app
  - `timezone` string, required
  - `posts` BulkPostItem[], required
    - `contentType` 'TEXT' | 'IMAGE' | 'VIDEO' | 'CAROUSEL', required: `DOCUMENT` posts cannot be bulk scheduled; create them one at a time with `POST /social-posts`
    - `text` string: Post text/caption for all platforms. On LinkedIn, mention a company page with @[Page Name](urn:li:organization:<id>), using the page name exactly as LinkedIn shows it
    - `platformTexts` PlatformText[]: Platform-specific text overrides
      - `platform` 'FACEBOOK' | 'INSTAGRAM' | 'THREADS' | 'TIKTOK' | 'TWITTER' | 'BLUESKY' | 'LINKEDIN' | 'PINTEREST' | 'YOUTUBE' | 'MASTODON' | 'TELEGRAM' | 'GOOGLE_BUSINESS' | 'TUMBLR', required
      - `text` string, required: On LinkedIn, mention a company page with @[Page Name](urn:li:organization:<id>)
    - `platformAspectRatios` PlatformAspectRatio[]: Reframe every image of an `IMAGE` or `CAROUSEL` post to one shape on a platform, one entry per platform. INSTAGRAM takes `SQUARE`, `PORTRAIT` or `LANDSCAPE` (Instagram refuses 9:16 feed and carousel images); TIKTOK takes all four. Other platforms, video posts, Instagram stories and a second entry for the same platform are rejected with a 400. Omit a platform to keep its images as uploaded. On an update it applies to the `mediaUrls` sent in the same request
      - `platform` 'FACEBOOK' | 'INSTAGRAM' | 'THREADS' | 'TIKTOK' | 'TWITTER' | 'BLUESKY' | 'LINKEDIN' | 'PINTEREST' | 'YOUTUBE' | 'MASTODON' | 'TELEGRAM' | 'GOOGLE_BUSINESS' | 'TUMBLR', required
      - `aspectRatio` 'SQUARE' | 'PORTRAIT' | 'VERTICAL' | 'LANDSCAPE', required: `SQUARE` is 1:1, `PORTRAIT` 4:5, `VERTICAL` 9:16 and `LANDSCAPE` 1.91:1
      - `fit` 'CROP' | 'PAD': How an image of another shape reaches the ratio. `CROP` (the default) cuts it from the center; `PAD` keeps the whole image and fills the rest with a blurred copy of it
    - `mediaUrls` string[]: Public HTTPS URLs of media to attach. Use POST /upload-urls to upload files first. A `DOCUMENT` post takes exactly one PDF, PPT, PPTX, DOC or DOCX URL, and a document file is only accepted on a `DOCUMENT` post. One uploaded URL may be reused across any number of posts; the file is kept until the last post referencing it has published. After publishing, the post's `mediaUrls` may hold expiring platform CDN links, so reuse the `publicUrl` you uploaded, not a URL read back from a published post
    - `mediaAltTexts` string[]: Alt text for each image, in the same order as mediaUrls (max 1000 characters each, empty string to skip one). Sent to X, Bluesky, Mastodon, Tumblr, LinkedIn, Facebook, Instagram, Threads and Pinterest (first image only, max 500 characters). TikTok, YouTube and Google Business Profile have no alt text field
    - `thumbnailUrl` string: Video thumbnail as an https URL or an image data URI, used on every platform that takes one unless `platformThumbnails` overrides it
    - `thumbnailTimestampMs` number: Video frame timestamp (ms) to use as the thumbnail, used by INSTAGRAM, PINTEREST and TIKTOK unless `platformThumbnails` overrides it
    - `platformThumbnails` PlatformThumbnail[]: A different thumbnail for one platform of a `VIDEO` post, one entry per platform. A platform without an entry uses the post-level `thumbnailUrl` and `thumbnailTimestampMs`. YOUTUBE, FACEBOOK and LINKEDIN take an image (`thumbnailUrl`); INSTAGRAM and PINTEREST take an image or a frame (`thumbnailTimestampMs`), the image winning when both are sent; TIKTOK takes only a frame. Other platforms, posts that are not videos, Facebook and Instagram stories, a second entry for one platform and an entry the platform cannot use are rejected with a 400. On an update, sending the array replaces every override and omitting it keeps the stored ones
      - `platform` 'FACEBOOK' | 'INSTAGRAM' | 'THREADS' | 'TIKTOK' | 'TWITTER' | 'BLUESKY' | 'LINKEDIN' | 'PINTEREST' | 'YOUTUBE' | 'MASTODON' | 'TELEGRAM' | 'GOOGLE_BUSINESS' | 'TUMBLR', required
      - `thumbnailUrl` string: The image to use as this platform's thumbnail, an https URL or an image data URI
      - `thumbnailTimestampMs` integer: The video frame (ms from the start) to use as this platform's thumbnail
    - `firstComments` PlatformFirstComment[]: A comment posted from the same account right after the post goes out on a platform, one entry per platform. Available on LINKEDIN (1250 characters), TWITTER (280, as a reply), THREADS (500, as a reply), BLUESKY (300, as a reply), MASTODON (500, as a reply) and YOUTUBE (10000). Other platforms, a platform the post does not go to, an empty or too long comment, a second entry for one platform, a private or made for kids YouTube video and Facebook or Instagram stories are rejected with a 400. The comment is posted about 10 seconds after the platform publishes (60 on YouTube) and retried up to 3 times; on X it is not retried, and a lifetime-deal account pays for it like any X post. A comment that fails never fails the post: read `firstCommentStatus` and `firstCommentError` on the platform. On an update, sending the array replaces every first comment and omitting it keeps the stored ones
      - `platform` 'FACEBOOK' | 'INSTAGRAM' | 'THREADS' | 'TIKTOK' | 'TWITTER' | 'BLUESKY' | 'LINKEDIN' | 'PINTEREST' | 'YOUTUBE' | 'MASTODON' | 'TELEGRAM' | 'GOOGLE_BUSINESS' | 'TUMBLR', required
      - `text` string, required: The comment's text, trimmed. The limit depends on the platform, see `firstComments`
    - `scheduledAt` string, date-time, required
    - `pinterestConfigs` PinterestPostConfig[]
      - `connectionId` string, required
      - `boardId` string: A board `id` from `GET /social-accounts/{id}/pinterest-boards`. Leave out both `boardId` and `boardName` to pin to a public board named AdaptlyPost, created on the account the first time it is needed
      - `boardName` string: The name of one of this account's boards, instead of `boardId`. AdaptlyPost looks it up when the post is created or updated and stores the board id. An exact match wins; otherwise the match ignores case. The request fails with 400 when no board has the name, when several do (the message lists their ids), or when `boardId` is also set. It never falls back to another board
      - `title` string
      - `link` string, uri
    - `tiktokConfigs` TikTokPostConfig[]
      - `connectionId` string, required
      - `title` string
      - `caption` string
      - `privacyLevel` 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY', required
      - `allowComments` boolean
      - `allowDuet` boolean
      - `allowStitch` boolean
      - `sendAsDraft` boolean
      - `aiGenerated` boolean
      - `brandedContent` boolean
      - `brandedContentOwnBrand` boolean
      - `autoAddMusic` boolean
    - `instagramConfigs` InstagramPostConfig[]
      - `connectionId` string, required
      - `postType` 'FEED' | 'REEL' | 'STORY': Where the post is published. `FEED` works with every content type, `REEL` needs `contentType: VIDEO`, and `STORY` needs `IMAGE`, `VIDEO` or `CAROUSEL`. A `CAROUSEL` story publishes each media item as its own story, in order. Any other combination is rejected with a 400.
      - `shareToStory` boolean: Also publish the same media as a story on this account, next to the `FEED` or `REEL` post. The story shows as its own entry in `platforms` with `postType: STORY`, so it succeeds, fails and retries on its own. It skips the caption, first comment, cover, collaborators, trial and audio, needs `IMAGE`, `VIDEO` or `CAROUSEL`, and a video must fit the story length limit (60 seconds). Ignored when `postType` is `STORY`.
      - `trialGraduation` 'MANUAL' | 'SS_PERFORMANCE': Publish the reel as a trial reel: Instagram shows it to non-followers first, and it stays off your followers' feeds and your profile grid until it is shared to them. `MANUAL` means you share it to followers yourself in the Instagram app; `SS_PERFORMANCE` means Instagram shares it automatically if it performs well. Omit it for a normal reel. Needs `contentType: VIDEO` and a `postType` of `REEL`, `FEED` or none; images, carousels and stories are rejected with a 400. Instagram decides which professional accounts can post trial reels and caps how many go out through the API; a refused trial fails that platform with an error saying so.
      - `collaborators` string[]: Instagram usernames invited as collaborators (Instagram Collab), up to 3. A leading `@` is stripped and duplicates are dropped. Works on feed images, carousels and reels; a story or a trial reel with collaborators is rejected with a 400. Each collaborator gets an invite in Instagram and must accept it before the post shows on their profile.
      - `aiGenerated` boolean: Disclose that the post was made or edited with AI. Instagram shows its "AI info" label on the post. Works on images, carousels, reels and stories, and a story made with `shareToStory` carries the label too. Omit it or send `false` for no label.
      - `audio` InstagramAudioConfig: Music or an original sound from Instagram's audio library, added to the Reel when it is published. Needs `contentType: VIDEO` and a `postType` other than `STORY`, and only works for accounts with `supportsInstagramAudio: true` (connected through Facebook). Find ids with `GET /social-accounts/{id}/instagram-audio`.
        - `audioId` string, required
        - `title` string: Track title, stored for display only
        - `artist` string: Artist or creator, stored for display only
        - `audioVolume` integer: Volume of the added audio. Defaults to 100
        - `videoVolume` integer: Volume of the video's own sound. Defaults to 100
    - `facebookConfigs` FacebookPostConfig[]
      - `pageId` string, required
      - `postType` 'FEED' | 'REEL' | 'STORY': Where the post is published. `FEED` works with every content type, `REEL` needs `contentType: VIDEO`, and `STORY` needs `IMAGE`, `VIDEO` or `CAROUSEL`. A `CAROUSEL` story publishes each media item as its own story, in order. Any other combination is rejected with a 400.
      - `shareToStory` boolean: Also publish the same media as a story on this account, next to the `FEED` or `REEL` post. The story shows as its own entry in `platforms` with `postType: STORY`, so it succeeds, fails and retries on its own. It skips the caption, video title and text background, needs `IMAGE`, `VIDEO` or `CAROUSEL`, and a video must fit the story length limit (60 seconds). Ignored when `postType` is `STORY`.
      - `videoTitle` string
      - `textFormatPresetId` string: Facebook's id for a colored text background (for example `106018623298955` is solid purple, `249307305544279` is a red to blue gradient). Only for `contentType: TEXT` posts with no media and text up to 130 characters; anything else is rejected with a 400. Links in the text show without a preview. If Facebook refuses the background, the post goes out as plain text.
      - `imageLayout` 'GRID' | 'CAROUSEL': How a post with several images shows on Facebook. `GRID` is a photo grid. `CAROUSEL` is a swipeable carousel where each image is a card with its own link, and needs 2 to 5 images, no videos, `postType` `FEED` and one `carouselCards` entry per image; anything else is rejected with a 400.
      - `carouselCards` FacebookCarouselCard[]: One card per image, in the same order as the media, used when `imageLayout` is `CAROUSEL`. Facebook only shows your image, title and description on a card whose link points to a website the Page owns; for any other link the post fails with the error Facebook returns.
        - `link` string, uri, required: The http or https URL the card opens
        - `title` string: The card's headline. Without it Facebook uses the linked page's title
        - `description` string: The line under the headline, such as a price or the domain. Without it Facebook uses the linked page
    - `youtubeConfigs` YouTubePostConfig[]
      - `connectionId` string, required
      - `postType` 'VIDEO' | 'SHORTS'
      - `videoTitle` string
      - `tags` string[]
      - `privacyStatus` 'public' | 'private' | 'unlisted'
      - `license` 'youtube' | 'creativeCommon'
      - `notifySubscribers` boolean
      - `allowEmbedding` boolean
      - `madeForKids` boolean
      - `categoryId` string
      - `playlistId` string
    - `googleBusinessConfigs` GoogleBusinessPostConfig[]
      - `connectionId` string, required
      - `topicType` 'STANDARD' | 'EVENT' | 'OFFER', required: `STANDARD` is an update. `EVENT` and `OFFER` need `eventTitle`, `eventStart` and `eventEnd`
      - `callToActionType` 'BOOK' | 'ORDER' | 'SHOP' | 'LEARN_MORE' | 'SIGN_UP' | 'CALL': Button shown on the post. Every type except `CALL` needs `callToActionUrl`; `CALL` dials the phone number on the Business Profile
      - `callToActionUrl` string, uri: Ignored when `callToActionType` is `CALL`
      - `eventTitle` string
      - `eventStart` string: `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm` in the business's local time, with no offset. Google reads event times in the location's time zone
      - `eventEnd` string: `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm` in the business's local time, with no offset
      - `offerCouponCode` string
      - `offerRedeemUrl` string, uri
      - `offerTerms` string
    - `tumblrConfigs` TumblrPostConfig[]
      - `connectionId` string, required
      - `tags` string[]: Tumblr tags, without commas. A leading `#` is stripped and duplicates are dropped, ignoring case
      - `title` string: Shown as a heading above the media and the text
      - `sourceUrl` string, uri: Shown as the source link of the post
  - `pageIds` string[]: Facebook pages to post to. Accepts either the account `id` or the `pageId` returned by /social-accounts
  - `tiktokConnectionIds` string[]
  - `threadsConnectionIds` string[]
  - `instagramConnectionIds` string[]
  - `twitterConnectionIds` string[]
  - `blueskyConnectionIds` string[]
  - `mastodonConnectionIds` string[]
  - `googleBusinessConnectionIds` string[]: Google Business Profile locations to post to, by the account `id` from /social-accounts. A location without an entry in `googleBusinessConfigs` gets a `STANDARD` update
  - `tumblrConnectionIds` string[]
  - `linkedinConnectionIds` string[]
  - `pinterestConnectionIds` string[]
  - `youtubeConnectionIds` string[]
  - `pinterestConfigs` PinterestPostConfig[]
    - `connectionId` string, required
    - `boardId` string: A board `id` from `GET /social-accounts/{id}/pinterest-boards`. Leave out both `boardId` and `boardName` to pin to a public board named AdaptlyPost, created on the account the first time it is needed
    - `boardName` string: The name of one of this account's boards, instead of `boardId`. AdaptlyPost looks it up when the post is created or updated and stores the board id. An exact match wins; otherwise the match ignores case. The request fails with 400 when no board has the name, when several do (the message lists their ids), or when `boardId` is also set. It never falls back to another board
    - `title` string
    - `link` string, uri
  - `tiktokConfigs` TikTokPostConfig[]
    - `connectionId` string, required
    - `title` string
    - `caption` string
    - `privacyLevel` 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY', required
    - `allowComments` boolean
    - `allowDuet` boolean
    - `allowStitch` boolean
    - `sendAsDraft` boolean
    - `aiGenerated` boolean
    - `brandedContent` boolean
    - `brandedContentOwnBrand` boolean
    - `autoAddMusic` boolean
  - `instagramConfigs` InstagramPostConfig[]
    - `connectionId` string, required
    - `postType` 'FEED' | 'REEL' | 'STORY': Where the post is published. `FEED` works with every content type, `REEL` needs `contentType: VIDEO`, and `STORY` needs `IMAGE`, `VIDEO` or `CAROUSEL`. A `CAROUSEL` story publishes each media item as its own story, in order. Any other combination is rejected with a 400.
    - `shareToStory` boolean: Also publish the same media as a story on this account, next to the `FEED` or `REEL` post. The story shows as its own entry in `platforms` with `postType: STORY`, so it succeeds, fails and retries on its own. It skips the caption, first comment, cover, collaborators, trial and audio, needs `IMAGE`, `VIDEO` or `CAROUSEL`, and a video must fit the story length limit (60 seconds). Ignored when `postType` is `STORY`.
    - `trialGraduation` 'MANUAL' | 'SS_PERFORMANCE': Publish the reel as a trial reel: Instagram shows it to non-followers first, and it stays off your followers' feeds and your profile grid until it is shared to them. `MANUAL` means you share it to followers yourself in the Instagram app; `SS_PERFORMANCE` means Instagram shares it automatically if it performs well. Omit it for a normal reel. Needs `contentType: VIDEO` and a `postType` of `REEL`, `FEED` or none; images, carousels and stories are rejected with a 400. Instagram decides which professional accounts can post trial reels and caps how many go out through the API; a refused trial fails that platform with an error saying so.
    - `collaborators` string[]: Instagram usernames invited as collaborators (Instagram Collab), up to 3. A leading `@` is stripped and duplicates are dropped. Works on feed images, carousels and reels; a story or a trial reel with collaborators is rejected with a 400. Each collaborator gets an invite in Instagram and must accept it before the post shows on their profile.
    - `aiGenerated` boolean: Disclose that the post was made or edited with AI. Instagram shows its "AI info" label on the post. Works on images, carousels, reels and stories, and a story made with `shareToStory` carries the label too. Omit it or send `false` for no label.
    - `audio` InstagramAudioConfig: Music or an original sound from Instagram's audio library, added to the Reel when it is published. Needs `contentType: VIDEO` and a `postType` other than `STORY`, and only works for accounts with `supportsInstagramAudio: true` (connected through Facebook). Find ids with `GET /social-accounts/{id}/instagram-audio`.
      - `audioId` string, required
      - `title` string: Track title, stored for display only
      - `artist` string: Artist or creator, stored for display only
      - `audioVolume` integer: Volume of the added audio. Defaults to 100
      - `videoVolume` integer: Volume of the video's own sound. Defaults to 100
  - `facebookConfigs` FacebookPostConfig[]
    - `pageId` string, required
    - `postType` 'FEED' | 'REEL' | 'STORY': Where the post is published. `FEED` works with every content type, `REEL` needs `contentType: VIDEO`, and `STORY` needs `IMAGE`, `VIDEO` or `CAROUSEL`. A `CAROUSEL` story publishes each media item as its own story, in order. Any other combination is rejected with a 400.
    - `shareToStory` boolean: Also publish the same media as a story on this account, next to the `FEED` or `REEL` post. The story shows as its own entry in `platforms` with `postType: STORY`, so it succeeds, fails and retries on its own. It skips the caption, video title and text background, needs `IMAGE`, `VIDEO` or `CAROUSEL`, and a video must fit the story length limit (60 seconds). Ignored when `postType` is `STORY`.
    - `videoTitle` string
    - `textFormatPresetId` string: Facebook's id for a colored text background (for example `106018623298955` is solid purple, `249307305544279` is a red to blue gradient). Only for `contentType: TEXT` posts with no media and text up to 130 characters; anything else is rejected with a 400. Links in the text show without a preview. If Facebook refuses the background, the post goes out as plain text.
    - `imageLayout` 'GRID' | 'CAROUSEL': How a post with several images shows on Facebook. `GRID` is a photo grid. `CAROUSEL` is a swipeable carousel where each image is a card with its own link, and needs 2 to 5 images, no videos, `postType` `FEED` and one `carouselCards` entry per image; anything else is rejected with a 400.
    - `carouselCards` FacebookCarouselCard[]: One card per image, in the same order as the media, used when `imageLayout` is `CAROUSEL`. Facebook only shows your image, title and description on a card whose link points to a website the Page owns; for any other link the post fails with the error Facebook returns.
      - `link` string, uri, required: The http or https URL the card opens
      - `title` string: The card's headline. Without it Facebook uses the linked page's title
      - `description` string: The line under the headline, such as a price or the domain. Without it Facebook uses the linked page
  - `youtubeConfigs` YouTubePostConfig[]
    - `connectionId` string, required
    - `postType` 'VIDEO' | 'SHORTS'
    - `videoTitle` string
    - `tags` string[]
    - `privacyStatus` 'public' | 'private' | 'unlisted'
    - `license` 'youtube' | 'creativeCommon'
    - `notifySubscribers` boolean
    - `allowEmbedding` boolean
    - `madeForKids` boolean
    - `categoryId` string
    - `playlistId` string
  - `googleBusinessConfigs` GoogleBusinessPostConfig[]
    - `connectionId` string, required
    - `topicType` 'STANDARD' | 'EVENT' | 'OFFER', required: `STANDARD` is an update. `EVENT` and `OFFER` need `eventTitle`, `eventStart` and `eventEnd`
    - `callToActionType` 'BOOK' | 'ORDER' | 'SHOP' | 'LEARN_MORE' | 'SIGN_UP' | 'CALL': Button shown on the post. Every type except `CALL` needs `callToActionUrl`; `CALL` dials the phone number on the Business Profile
    - `callToActionUrl` string, uri: Ignored when `callToActionType` is `CALL`
    - `eventTitle` string
    - `eventStart` string: `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm` in the business's local time, with no offset. Google reads event times in the location's time zone
    - `eventEnd` string: `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm` in the business's local time, with no offset
    - `offerCouponCode` string
    - `offerRedeemUrl` string, uri
    - `offerTerms` string
  - `tumblrConfigs` TumblrPostConfig[]
    - `connectionId` string, required
    - `tags` string[]: Tumblr tags, without commas. A leading `#` is stripped and duplicates are dropped, ignoring case
    - `title` string: Shown as a heading above the media and the text
    - `sourceUrl` string, uri: Shown as the source link of the post

## Response `201`

Bulk scheduling result

- object
  - `totalScheduled` integer
  - `totalFailed` integer
  - `results` object[]
    - `postId` string
    - `success` boolean
    - `isScheduled` boolean
    - `scheduledAt` string, date-time
    - `errorMessage` string

## Other responses

- `400`: Validation error
- `401`: Invalid or missing API key
- `403`: The key's role does not hold `posts.schedule`
- `429`: Rate limit exceeded

## Changes

- **2026-10-19** `8390a5fa9a0d`: 4 info
  - added the new optional request property `facebookConfigs/items/carouselCards`
  - added the new optional request property `facebookConfigs/items/imageLayout`
  - added the new optional request property `posts/items/facebookConfigs/items/carouselCards`
  - added the new optional request property `posts/items/facebookConfigs/items/imageLayout`

[Operation history](https://skmtc.dev/adaptlypost/apis/adaptlypost-public-api/changes/social-posts/bulk/post.md)

---

[API](https://skmtc.dev/adaptlypost/apis/adaptlypost-public-api.md) · [All operations](https://skmtc.dev/adaptlypost/apis/adaptlypost-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/adaptlypost/apis/adaptlypost-public-api/revisions/8390a5fa9a0d?raw)
