POST/social-posts
Creates and schedules one or more social posts (up to 15 per request).
pf-api-keyRate limit: 1,260 requests per day, 180 per minuteMaximum 250 MB per video and 10 MB per image.Disabled accounts
If a post's socialMediaId points to an account whose connectionStatus is DISABLED, scheduling it returns 400 Bad Request. You can still save it as a draft (status: DRAFT). Check connectionStatus via GET /social-media/my-social-accounts first.
A full post queue returns 403
POST /social-posts returns 403 with message sub.<plan>.maxScheduledPosts (<plan> is starter, creator, growth, pro or enterprise) when the account's queue is full: the queue counts posts scheduled to publish, approved or waiting for approval, across all of the account's workspaces. Drafts, failed posts and published posts don't count. description names the plan's ceiling and how many posts are queued now; every plan's ceiling is on postfa.st/fair-usage.
A request that starts under the ceiling is created whole (up to 15 posts), so the queue can go a few posts over before the next request is refused.
Stop sending scheduled posts and don't retry in a loop: this is not a rate limit and carries no Retry-After. Send again once queued posts have published, save the posts as drafts (status: DRAFT; drafts have their own limit), or move to a bigger plan.
{
"statusCode": 403,
"message": "sub.starter.maxScheduledPosts",
"error": "FORBIDDEN",
"description": "Your Starter plan holds 150 queued posts at a time, and 150 are queued now. A slot frees up each time a queued post publishes."
}X (Twitter) per-account daily cap
PostFast caps X API posts per account per day: 6 on Starter, Creator, and Growth; 9 on Pro and Enterprise. An organization-wide daily X cap also applies. These are separate from the request rate limits.
Daily posting limits
Each platform has its own per-account daily posting limit, resetting at midnight UTC. See the fair usage policy at postfa.st/fair-usage for details.
Geo controls are shared across every post in a request
The controls object is applied to every post in the posts[] array. Because each geo field is platform-specific and validated per post, batch by platform when using geotags or country limits: send Facebook posts in one request and Instagram posts in another, rather than mixing platforms whose controls do not apply to the other.
Resolve place IDs with GET /social-media/search-places; the same id works as both facebookPlaceId and instagramLocationId.
The Threads topic is shared across every post in a request
Like every key in controls, threadsTopicTag applies to every post in the posts[] array. Other platforms ignore it, so it is safe in a mixed-platform batch, but Threads posts that need different topics have to be sent in separate requests.
YouTube language and captions are shared across every post in a request
youtubeLanguage and youtubeCaptionKey apply to every post in the posts[] array, like every key in controls. Other platforms ignore them, but videos in different languages, each with its own caption file, have to be sent in separate requests.
Facebook link carousels
Set facebookCarouselMainLink in controls on a Facebook post with 2 to 10 images and it publishes as a link carousel: each image is a card that links out. With one image it posts as a normal photo post. The main link is the post's link and the destination of any card without its own mediaLink.
Each mediaItems[] entry can carry mediaLink (where the card leads), mediaCaption (the card's headline) and mediaDescription (the line under it). Keep the headline to 32 characters and the description to 18, the limits the PostFast app uses for carousel cards.
A link carousel takes images only. Set facebookCarouselShowEndCard to true to add Facebook's end card after the last card; it is off by default.
{
"posts": [
{
"content": "Two new guides are up. Swipe to pick one.",
"mediaItems": [
{
"key": "image/b1c2d3e4-f5a6-7890-1234-567890abcdef.jpg",
"type": "IMAGE",
"sortOrder": 0,
"mediaLink": "https://example.com/guides/scheduling",
"mediaCaption": "Scheduling basics",
"mediaDescription": "5 minute read"
},
{
"key": "image/c2d3e4f5-a6b7-8901-2345-67890abcdef1.jpg",
"type": "IMAGE",
"sortOrder": 1,
"mediaLink": "https://example.com/guides/analytics",
"mediaCaption": "Reading your analytics",
"mediaDescription": "8 minute read"
}
],
"scheduledAt": "2026-12-01T10:00:00.000Z",
"socialMediaId": "660f9500-f3ac-42e5-b827-556766550002"
}
],
"controls": {
"facebookContentType": "POST",
"facebookCarouselMainLink": "https://example.com/guides",
"facebookCarouselShowEndCard": true
}
}Unknown fields are ignored
Unknown fields in the request body are ignored, not rejected. If a setting doesn't take effect, check the field name against this page.
Request body
postsarrayrequiredArray of post objects (1-15 per request).
contentstringrequiredText content of the post.
mediaItemsarrayoptionalMedia attached to the post.
keystringrequiredS3 object key from /file/get-signed-upload-urls.
typestringrequiredWhether the item is an image or a video.
IMAGEVIDEOsortOrdernumberrequiredOrdering of the media items.
coverTimestampstringoptionalMillisecond timestamp used as the video cover (e.g. 5000). Supported on TikTok, Instagram Reels, and Pinterest video pins; not supported on Facebook.
coverImageKeystringoptionalS3 key of a custom video cover/thumbnail. Takes priority over coverTimestamp where supported (Instagram Reels JPEG max 8MB; Facebook Reels max 10MB; Pinterest JPEG/PNG; YouTube uses youtubeThumbnailKey; not supported on TikTok).
mediaLinkstringoptionalFacebook link carousels: where this card leads. Falls back to controls.facebookCarouselMainLink.
mediaCaptionstringoptionalFacebook link carousels: the card's headline. Keep it to 32 characters, the limit the PostFast app uses.
mediaDescriptionstringoptionalFacebook link carousels: the line under the headline. Keep it to 18 characters, the limit the PostFast app uses. Used as the headline when mediaCaption is empty.
scheduledAtISO 8601Required (unless DRAFT)UTC date-time, e.g. YYYY-MM-DDTHH:mm:ss.sssZ. Must be in the future, within 1 year.
socialMediaIdstring (UUID)requiredTarget account id from /social-media/my-social-accounts.
firstCommentstringoptionalAutomatic first comment, posted ~10s after publish (up to 3 attempts). Supported on X, Instagram, Facebook, YouTube, Threads, and TikTok. On TikTok the cap is 1,200 chars and comments must be enabled on the post; a TikTok account that can't post comments returns firstComment.tiktok.notSupported, in which case reconnect the account. See the first-comment limits table below.
statusstringoptionaldefault SCHEDULEDWhether the post is scheduled or saved as a draft.
DRAFTSCHEDULEDapprovalStatusstringoptionaldefault APPROVEDAPPROVED publishes; PENDING_APPROVAL holds for review.
APPROVEDPENDING_APPROVALcontrolsobjectoptionalPlatform-specific settings (see the controls matrix below).
Controls (platform-specific)
Optional controls object. Each field applies only to its platform; PostFast validates which controls are valid for the target account.
X (Twitter)
xRetweetUrlstringoptionalURL of a tweet to retweet without changes. Content and media are ignored when set. Accepts x.com, twitter.com, mobile.twitter.com.
instagramPublishTypestringoptionaldefault TIMELINEWhere the media is published.
TIMELINESTORYREELinstagramPostToGridbooleanoptionaldefault trueAlso post to the profile grid.
instagramCollaboratorsstring[]optionalInstagram usernames invited as collaborators.
instagramTrialReelStrategystringoptionalPublish the reel as a trial (non-followers only). Requires instagramPublishType: REEL; cannot combine with collaborators.
MANUALSS_PERFORMANCEinstagramLocationIdstringoptionalGeotag the post with a physical place. Same numeric ID returned by /social-media/search-places (the value also used for facebookPlaceId). Works on a single image, video, reel, or story - not on carousels (2+ media items).
instagramLocationNamestringoptionalOptional display label for the location. Stored for your portal/UI; never sent to Meta.
instagramIsAiGeneratedbooleanoptionaldefault falseAdds Instagram's "AI info" label to the post. Set it when the image or video was AI-generated. Applies to images, videos, reels, stories, and carousels.
instagramLocationId works on a single image, video, reel, or story - not on carousels (2 or more media items).
instagramIsAiGenerated labels the whole post, not individual carousel slides, and cannot be added or removed after publishing.
facebookContentTypestringoptionaldefault POSTPOST = up to 10 photos or 1 video; REEL = 1 video; STORY = 1 image or video.
POSTREELSTORYfacebookCarouselMainLinkstringoptionalPublishes a Facebook post with 2 to 10 images as a link carousel. The post's main link, used by any card without its own mediaLink. Images only.
facebookCarouselShowEndCardbooleanoptionaldefault falseAdds Facebook's end card after the last card of a link carousel.
facebookReelsCollaboratorsstring[]optionalFacebook usernames invited to a Reel collaboration.
facebookPlaceIdstringoptionalGeotag the post with a physical place. Numeric Facebook Page ID with location data, resolved via /social-media/search-places. Feed posts only (text/photo/carousel) - not Reels, Stories, or video posts.
facebookPlaceNamestringoptionalOptional display label for the place. Stored for your portal/UI; never sent to Meta.
facebookTargetCountriesstring[]optionalRestrict who can see the post by country. Array of ISO 3166-1 alpha-2 codes (case-insensitive), max 25, e.g. ["BG", "DE", "AT"]. Feed posts only. Can be combined with a geotag.
Geotag (facebookPlaceId) and country limit (facebookTargetCountries) both apply to feed posts only - not Reels, Stories, or video posts.
facebookTargetCountries is audience gating, not a hint: only people in the listed countries can see the post, and it is hidden from anyone not logged in to Facebook, so total reach drops.
Threads
threadsTopicTagstringoptionalThe post's topic on Threads. One topic per post: 1-50 characters, with no . or &. Only Threads posts use it. An invalid topic returns threadsTopicTag.invalid (400); over 50 characters is rejected earlier by input validation (also 400).
The topic shows as the post's topic in Threads. If the text also contains a #hashtag, the topic you set wins and the hashtag stays plain text.
TikTok
tiktokTitlestringoptionalCarousel title (max 90 chars, photo carousels only). When set, content becomes the description.
tiktokPrivacystringoptionaldefault PUBLICDeprecated. On TikTok, videos use the account's default privacy (no per-post control); photos default to public. To publish privately, save a draft with tiktokIsDraft: true. Still accepted for backward compatibility.
PUBLICMUTUAL_FRIENDSFOLLOWER_OF_CREATORONLY_MEtiktokIsDraftbooleanoptionaldefault falseSave to TikTok drafts instead of publishing.
tiktokAllowCommentsbooleanoptionaldefault trueAllow comments.
tiktokAllowDuetbooleanoptionaldefault trueAllow duets.
tiktokAllowStitchbooleanoptionaldefault trueAllow stitches.
tiktokBrandOrganicbooleanoptionaldefault falseYour own brand promotion.
tiktokBrandContentbooleanoptionaldefault falsePaid partnership / branded content.
tiktokAutoAddMusicbooleanoptionaldefault falseLet TikTok add music automatically (photo posts only; ignored on videos).
tiktokMusicSoundIdstringoptionalAttach a specific licensed track to a photo carousel or video. Takes a musicSoundId from /tiktok-sounds (max 128 chars); TikTok attaches the audio server-side at publish. On a video, the track plays at 50% volume over the video's original sound, which also plays at 50%. Cannot be combined with tiktokAutoAddMusic.
tiktokMusicSoundNamestringoptionalOptional display label for the chosen track (max 256 chars), e.g. "Track - Artist". Stored for your portal/UI; never sent to TikTok.
tiktokIsAigcbooleanoptionaldefault falseDisclose AI-generated content.
Sound selection applies to TikTok photo carousels and videos. Resolve the id with GET /social-media/:id/tiktok-sounds, which only returns pre-cleared Commercial Music Library tracks.
tiktokMusicSoundId and tiktokAutoAddMusic: true are mutually exclusive and return tiktokMusic.conflictAutoAddMusic together. Set one or neither. With neither, a photo post publishes silent and a video keeps its own audio.
Neither music control is applied to drafts (tiktokIsDraft: true), because TikTok ignores post settings on drafts.
YouTube
youtubeTitlestringoptionalVideo title (max 100 chars). Over-length titles are rejected with youtubeTitle.tooLong (400), never truncated. When omitted, the first line of content becomes the title, clamped to 100 chars.
youtubePrivacystringoptionaldefault PUBLICVisibility of the video.
PRIVATEPUBLICUNLISTEDyoutubeTagsstring[]optionalVideo tags.
youtubeCategoryIdstringoptionalYouTube category id.
youtubeIsShortbooleanoptionaldefault truePublish as a Short.
youtubeMadeForKidsbooleanoptionaldefault falseCOPPA made-for-kids flag.
youtubePlaylistIdstringoptionalPlaylist to add the video to after publishing (from /youtube-playlists). The playlist must belong to the authenticated channel; the video still publishes if the playlist addition fails.
youtubeThumbnailKeystringoptionalCustom thumbnail media key (JPEG or PNG, up to 50MB, the formats and size YouTube's thumbnail API accepts; YouTube recommends 3840x2160). Requires a phone-verified channel; the video still publishes if the thumbnail upload fails.
youtubeContainsSyntheticMediabooleanoptionaldefault falseDiscloses that the video contains realistic altered or synthetic (AI) content. Sent to YouTube only when true.
youtubeLanguagestringoptionalThe video's language as a BCP-47 code, e.g. en, en-GB, es, es-419, fr or pt-BR. Sets both YouTube's "Video language" (the spoken language) and its "Title and description language". PostFast normalizes the code before sending it (pt-br becomes pt-BR). An unknown code returns youtubeLanguage.invalid (400), naming the code. Only YouTube posts use it. Required when youtubeCaptionKey is set.
youtubeCaptionKeystringoptionalKey of an uploaded .srt or .vtt caption file (file/<uuid>.srt or file/<uuid>.vtt) from /file/get-signed-upload-urls. It is added to the video as a caption track in the youtubeLanguage language right after the video uploads. Viewers see the track named by its language, and YouTube's automatic captions stay available separately. The file must be timed SRT or WebVTT in plain UTF-8, max 10 MB. Requires youtubeLanguage. If adding the captions fails, the video still publishes.
youtubeContainsSyntheticMedia, youtubeLanguage, and youtubeCaptionKey are set at creation only. There is no update route for a post, so they cannot be changed afterwards.
Captions need a language: youtubeCaptionKey always goes with youtubeLanguage, and the track is added in that language. One caption track per video.
Upload the caption file before creating the post: request a URL from /file/get-signed-upload-urls with application/x-subrip (.srt) or text/vtt (.vtt), then PUT the file to it. Creating the post checks that the file exists, is at most 10 MB, and is timed SRT or WebVTT in plain UTF-8. If the captions still can't be added when the video publishes, the video publishes without them.
pinterestBoardIdstringRequired for PinterestBoard to publish the pin to (from /pinterest-boards).
pinterestLinkstringoptionalDestination URL when users click the pin.
Content parsing: first line becomes the pin title (max 100 chars); the rest becomes the description (max 800).
Carousel pins support 2-5 static images (no video).
linkedinAttachmentKeystringRequired for documentsS3 key from /file/get-signed-upload-urls for a PDF/DOC/DOCX/PPT/PPTX (max 60MB). Cannot mix with mediaItems.
linkedinAttachmentTitlestringoptionaldefault DocumentDisplay title for the document.
Google Business Profile
gbpLocationIdstringRequired for GBPLocation resource name (from /gbp-locations).
gbpTopicTypestringoptionaldefault STANDARDPost type.
STANDARDEVENTOFFERgbpCallToActionTypestringoptionalCTA button.
BOOKORDERSHOPLEARN_MORESIGN_UPCALLgbpCallToActionUrlstringoptionalDestination URL for the CTA button. Used when a CTA type other than CALL is set.
gbpEventTitlestringoptionalTitle for EVENT/OFFER posts (max 58 chars).
gbpEventStartDateISO 8601Required for EVENT/OFFERStart of the event or offer.
gbpEventEndDateISO 8601Required for EVENT/OFFEREnd of the event or offer.
gbpOfferCouponCodestringoptionalCoupon code (OFFER only).
gbpOfferRedeemUrlstringoptionalRedemption URL (OFFER only).
gbpOfferTermsstringoptionalTerms and conditions (OFFER only).
1 image only (JPEG/PNG), no video or carousel. Content limit 1500 chars.
Standard posts are visible for 6 months; Event and Offer posts stay until their end date.
The call-to-action button is ignored on OFFER posts.
Platform character limits
| Platform | `content` cap | Notes |
|---|---|---|
| 63,206 | ||
| X (Twitter) | 4,000 | Premium default; free-tier accounts are capped at 280 by X. |
| 2,200 | ||
| 3,000 | ||
| TikTok | 4,000 / 2,200 | Photo carousels: 4,000. Narrows to 2,200 if any media item is a VIDEO. See also tiktokTitle (max 90, photo carousels only). |
| YouTube | 5,000 | < and > are not allowed in titles or descriptions. See also youtubeTitle (max 100). |
| BlueSky | 300 | |
| Threads | 500 | |
| 900 | First line is the pin title (max 100); the remaining 800 chars become the description. | |
| Telegram | 4,096 | |
| Google Business Profile | 1,500 |
Exceeding a cap returns 400 Bad Request with a contentLength.<platform> error key.
First-comment limits
| Platform | `firstComment` cap |
|---|---|
| 2,200 | |
| 8,000 | |
| YouTube | 10,000 |
| Threads | 500 |
| X (Twitter) | 4,000 (Premium); 280 (free tier) |
| TikTok | 1,200 |
On TikTok, firstComment requires comments enabled on the post (tiktokAllowComments); if comments are disabled the comment is silently skipped at publish time, and a TikTok account that can't post comments returns firstComment.tiktok.notSupported. firstComment is not supported on LinkedIn, Pinterest, BlueSky, or Google Business Profile and returns a validation error.
Geo controls validation errors
| Situation | Error |
|---|---|
| Geo field sent for the wrong platform | facebookPlaceId.notSupported / instagramLocationId.notSupported / facebookTargetCountries.notSupported |
| Geotag on a Facebook Reel or Story | facebookPlaceId.contentType.notSupported |
| Geotag on a Facebook video post | facebookPlaceId.video.notSupported |
| Geotag on an Instagram carousel | instagramLocationId.carousel.notSupported |
| Country limit on a Facebook Reel or Story | facebookTargetCountries.contentType.notSupported |
| More than 25 countries | facebookTargetCountries.tooMany |
| Invalid country code (not a real ISO 3166-1 alpha-2) | 400 - rejected by input validation; the message lists the allowed values |
| Non-numeric place/location ID | facebookPlaceId.invalidId / instagramLocationId.invalidId |
The facebookPlaceName and instagramLocationName fields are optional, display-only, and stored so the place name shows in your portal. They are never sent to Meta - only the IDs and country codes are.
YouTube language and caption errors
| Situation | Error |
|---|---|
| Caption file missing, empty, over 10 MB, or not timed SRT or WebVTT in plain UTF-8 | media.invalidMedia (400), naming the file |
youtubeCaptionKey is not a file/<uuid>.srt or .vtt upload (checked on every platform) | youtubeCaptionKey.invalid (400) |
YouTube post with youtubeCaptionKey but no youtubeLanguage | youtubeCaptionKey.languageRequired (400) |
| Unknown language code on a YouTube post | youtubeLanguage.invalid (400), naming the code |
| YouTube refuses the language when the video publishes (rare, since unknown codes stop at creation) | The post fails once, without retrying. lastError.message: "YouTube doesn't accept the video language set on this post. Pick another language, or clear it, and try again." |
A caption key outside file/ is rejected earlier by input validation (also 400). To replace a rejected caption file, upload the corrected file and use its new key.
Response
201 Created An object with the ids of the created posts.