Every platform has its own knobs — Instagram’s REEL vs FEED, X’s
replySettings, TikTok’s privacy and consent flags, YouTube’s
madeForKids, LinkedIn’s PDF carousels. Postbreeze exposes these
through a single field on each platform target:
platforms: [
{
accountId: "soc_…",
platformOptions: {
platform: "INSTAGRAM",
kind: "REEL",
},
},
]
platformOptions is a discriminated union — the platform field
picks which shape applies. Pass only the fields you want to override;
everything else falls back to a sensible default.
These live next to platformOptions, not inside it. They apply to
any platform that supports them.
| Field | Type | Description |
|---|
accountId | string | Required. The SocialAccount.id to publish to. |
captionOverride | string (≤10,000) | Per-platform caption that overrides the post-level content. Useful when one platform needs a different tone, length, or set of hashtags. |
firstComment | string (≤2,000) | Auto-posts as the first reply once the main post is live. Supported on Instagram, Facebook, X, YouTube, TikTok, LinkedIn, Threads. Ignored on platforms that don’t have a comments surface. |
platformOptions | object | Platform-specific options (this page). Discriminated by platform. |
mediaIds | string[] | Overrides the post-level media for this target only. Use when one platform needs a different crop or asset. |
{
"accountId": "soc_…",
"captionOverride": "Different caption for X — shorter, punchier.",
"firstComment": "Drop a 🔥 if you want the deep-dive thread.",
"platformOptions": { "platform": "X", "replySettings": "following" }
}
The full Zod schema lives in
packages/shared/src/index.ts.
This page is the human-readable copy.
Instagram
| Field | Type | Description |
|---|
platform | "INSTAGRAM" | Discriminator. Required. |
kind | "FEED" | "REEL" | Which Instagram surface to publish to. Defaults to "FEED". |
{
"platform": "INSTAGRAM",
"kind": "REEL"
}
Constraints
- 📐 Feed posts require aspect ratio between 4:5 (0.8) and 1.91:1.
- 📱 Reels must be a single 9:16 video, 3–90 seconds.
- 🎠 Feed carousels support up to 10 media items.
- 🚫 Stories are not supported in v1 — they’re rejected at validation.
- 🚫 Mixing image + video in one Feed post is rejected by the API.
- 💬
firstComment is supported on Feed posts and Reels (not stories — and stories don’t ship in v1 anyway).
Facebook
| Field | Type | Description |
|---|
platform | "FACEBOOK_PAGE" | Discriminator. Required. |
kind | "FEED" | "PHOTO" | "PHOTO_CAROUSEL" | "VIDEO" | "REEL" | Publishing surface. Defaults to "FEED". With media attached, FEED is auto-normalized to PHOTO or PHOTO_CAROUSEL. |
link | string (URL) | Optional outbound link. With kind: "FEED", Facebook auto-generates the link preview card from the URL’s Open Graph tags. |
reelTitle | string (≤2,200) | Optional Reel title prepended to the description on publish. |
{
"platform": "FACEBOOK_PAGE",
"kind": "REEL",
"reelTitle": "Behind the launch"
}
Constraints
- 🚫 Cannot mix videos and images in the same post.
- ✅ Up to 10 images for Feed photo carousels.
- 🎬 Reels must be 9:16, between 3 and 90 seconds.
- 📊 Reels are capped at 30 publishes per Page per 24h by Meta.
- 🔗 Use
link (with kind: "FEED") for OG-card link previews.
- 💬
firstComment is supported on every surface except Reels.
| Field | Type | Description |
|---|
platform | "X" | Discriminator. Required. |
replySettings | "everyone" | "following" | "mentionedUsers" | Who can reply. Defaults to "everyone". Enforced server-side by X. |
threadParts | string[] (each ≤4,000, up to 25 items) | Tail of a thread. When non-empty, each entry posts as a separate tweet threaded under the first. content is the FIRST tweet in the thread. |
replyToId | string | Optional tweet ID — turns this post into a reply to an existing tweet. |
{
"platform": "X",
"replySettings": "following",
"threadParts": [
"Here's the second tweet in the thread.",
"And the third — wrapping up with the link → https://example.com"
]
}
Constraints
- 🖼️ Up to 4 images or 1 video per tweet — never a mix.
- 🧵
threadParts adds up to 25 tweets after the root. Each entry inherits no media; only the root tweet carries the post’s mediaItems.
- 💬
firstComment is published as a reply to the root tweet, not the last thread part.
- 🌍 Tweet text is always globally visible — X doesn’t expose geo-restriction on text.
LinkedIn (Personal)
| Field | Type | Description |
|---|
platform | "LINKEDIN_PERSON" | Discriminator. Required. |
visibility | "PUBLIC" | "CONNECTIONS" | Who can see the post. Defaults to "PUBLIC". |
postAsPdfCarousel | boolean | Composite the attached images into a single PDF and post as a “document”. LinkedIn renders it as a swipeable carousel under the post. |
pdfCarouselTitle | string (≤100) | Title shown above the PDF carousel. Required when postAsPdfCarousel is true. |
{
"platform": "LINKEDIN_PERSON",
"visibility": "PUBLIC",
"postAsPdfCarousel": true,
"pdfCarouselTitle": "Q2 launch retro"
}
Constraints
- 🖼️ Up to 20 images per post.
- 🚫 Multi-video posts are not supported.
- 📄 Single PDF document posts supported — upload the PDF and attach with
type: "document". See Media uploads → LinkedIn PDFs.
- 🔗 Link previews are auto-generated when no media is attached.
LinkedIn (Company)
Same shape as LinkedIn Personal, with the discriminator
platform: "LINKEDIN_COMPANY". Use for organization pages the
connected user administers.
| Field | Type | Description |
|---|
platform | "LINKEDIN_COMPANY" | Discriminator. Required. |
visibility | "PUBLIC" | "CONNECTIONS" | Kept for shape parity — company actors always post publicly, no matter what’s passed. |
postAsPdfCarousel | boolean | See LinkedIn Personal above. |
pdfCarouselTitle | string (≤100) | Title shown above the PDF carousel. |
{
"platform": "LINKEDIN_COMPANY",
"postAsPdfCarousel": true,
"pdfCarouselTitle": "Quarterly numbers"
}
Constraints
- 🏢 One
SocialAccount per administered organization. Use the
per-org accountId to fan out to multiple pages.
- 📊 Org-page analytics require LinkedIn Marketing Developer Platform
approval — until that lands, the analytics surface shows an
“approval pending” banner. Posting still works.
TikTok (Personal)
Required by TikTok. privacy has no default — you must pass
one of the four values below. Branded-content (brandContentToggle: true) is only allowed with privacy: "PUBLIC_TO_EVERYONE".
| Field | Type | Description |
|---|
platform | "TIKTOK_PERSONAL" | Discriminator. Required. |
privacy | "PUBLIC_TO_EVERYONE" | "MUTUAL_FOLLOW_FRIENDS" | "FOLLOWER_OF_CREATOR" | "SELF_ONLY" | Required. TikTok’s Content Sharing Guidelines forbid a default here — the creator must pick a value their creator_info API confirmed is allowed for their account. |
allowComments | boolean | Whether viewers can comment. Defaults to false. |
allowDuet | boolean | Whether viewers can duet. Defaults to false. |
allowStitch | boolean | Whether viewers can stitch. Defaults to false. |
autoAddMusic | boolean | Let TikTok auto-add a recommended music track. Defaults to false. Photo posts only. |
brandOrganicToggle | boolean | ”Your Brand” disclosure — content promotes the creator’s own brand. Defaults to false. Surfaced to TikTok as brand_organic_toggle. |
brandContentToggle | boolean | ”Branded Content” disclosure — paid partnership / third-party brand. Defaults to false. Surfaced to TikTok as brand_content_toggle. |
photoTitle | string (≤90 UTF-16 runes) | Photo posts only — TikTok exposes a short metadata title separate from the caption. Ignored for video posts. |
{
"platform": "TIKTOK_PERSONAL",
"privacy": "PUBLIC_TO_EVERYONE",
"allowComments": true,
"allowDuet": false,
"allowStitch": false,
"brandContentToggle": true
}
Constraints
- 🎬 Videos publish via
PULL_FROM_URL; the host of the media URL must be on TikTok’s verified-domain list (your R2 public bucket).
- 📸 Photo carousels support up to 35 images.
- 📝 Video captions: up to 2,200 characters. Photo titles capped at 90 characters — use
content for longer descriptions.
- 🔒 While the app is in TikTok Sandbox,
privacy is forced to "SELF_ONLY" regardless of what you pass — that’s a TikTok constraint until App Review lands.
- 🤝 Branded content cannot be
SELF_ONLY, MUTUAL_FOLLOW_FRIENDS, or FOLLOWER_OF_CREATOR. Validation rejects the combination pre-flight.
TikTok (Business)
Same shape as TikTok Personal, with the discriminator
platform: "TIKTOK_BUSINESS". Use for accounts authenticated through
TikTok for Business.
{
"platform": "TIKTOK_BUSINESS",
"privacy": "PUBLIC_TO_EVERYONE",
"allowComments": true,
"brandContentToggle": false
}
The reserved TIKTOK_BUSINESS slot exists for accounts that grant
the Business OAuth flow; the publish shape and validation rules are
identical to Personal.
YouTube
| Field | Type | Description |
|---|
platform | "YOUTUBE" | Discriminator. Required. |
visibility | "PUBLIC" | "UNLISTED" | "PRIVATE" | Defaults to "PUBLIC". |
madeForKids | boolean | COPPA flag — required by YouTube on every upload. Videos marked made-for-kids have restricted features (no comments, no notifications, limited ad targeting). Defaults to false. |
youtubeTitle | string (≤100) | Optional title that overrides the first line of content. YouTube splits title from description; this field carries the override. |
{
"platform": "YOUTUBE",
"visibility": "UNLISTED",
"madeForKids": false,
"youtubeTitle": "Launch day — full walkthrough"
}
Constraints
- 🎬 YouTube targets accept a single video, no images.
- ⏱️ Videos ≤ 3 minutes in 9:16 are automatically published as YouTube Shorts; longer videos publish as regular videos. Postbreeze doesn’t override this — it’s YouTube’s classification.
- 🖼️ Custom thumbnails work for regular videos only (not Shorts).
- 💬
firstComment is supported and posted as a top-level comment after upload completes.
- 🚫 Tags, category overrides, AI-disclosure flag (
containsSyntheticMedia) are not exposed in v1 — defaults are applied server-side (categoryId: "22" “People & Blogs”).
Pinterest
| Field | Type | Description |
|---|
platform | "PINTEREST" | Discriminator. Required. |
board | string | Pinterest board ID. Required by the API for any pin. |
title | string (≤100) | Optional pin title — distinct from the post content, which becomes the pin description. |
link | string (URL) | Optional outbound link the pin opens when clicked. |
{
"platform": "PINTEREST",
"board": "1234567890123456789",
"title": "Spring lookbook",
"link": "https://example.com/spring"
}
Constraints
- 📌 Every pin requires a
board. There is no default board fallback in v1.
- 🖼️ Pinterest carousels accept up to 5 images.
- 🚫 Video pins are not supported in v1.
- 🔗
link becomes the pin’s destination — leave it unset for image-only inspiration pins.
Threads
| Field | Type | Description |
|---|
platform | "THREADS" | Discriminator. Required. |
kind | "TEXT" | "IMAGE" | "VIDEO" | "CAROUSEL" | Publishing surface. Defaults to "TEXT". The compose flow normally auto-derives it from the attached media (no media → TEXT, single image → IMAGE, etc.); set it explicitly if you want to force a surface. |
threadParts | string[] (each ≤500, up to 25 items) | Optional thread tail. When non-empty, each entry posts as a separate Threads post chained via reply_to_id under the previous one. content is the FIRST post in the thread. |
{
"platform": "THREADS",
"kind": "TEXT",
"threadParts": [
"Second post in the thread — explaining the why.",
"Third post — the punchline."
]
}
Constraints
- 📝 Each post (root + every follow-up) is capped at 500 characters — Threads has no “See more” fold. The publisher rejects pre-flight if any part is over.
- 🧵
threadParts adds up to 25 follow-ups after the root. Each follow-up is text-only; only the root post carries the attached mediaItems.
- 🎠 Carousels accept 2–20 mixed items (images and videos) on the root post.
- 🎬 Videos up to 5 minutes / 1 GB.
- 📊 Posting limits: 250 published posts / 24h, 1,000 replies / 24h. Each follow-up counts toward the reply quota.
- 💬
firstComment is posted as a reply to the root post, not the last thread part.
One post fanned out to four platforms, each with its own
platformOptions, per-target caption override, and first-comment:
await postbreeze.posts.create({
content: "We just launched 🚀",
mediaItems: [{ url: heroUrl, type: "image" }],
platforms: [
{
accountId: "soc_ig_…",
firstComment: "Link in bio! 🔗",
platformOptions: { platform: "INSTAGRAM", kind: "FEED" },
},
{
accountId: "soc_x_…",
captionOverride: "We just launched 🚀 → https://example.com",
platformOptions: {
platform: "X",
replySettings: "everyone",
threadParts: [
"Here's what's new — a thread 🧵",
"Schedule once, publish everywhere. No spreadsheet required.",
],
},
},
{
accountId: "soc_li_…",
firstComment: "What do you think? Drop a comment below 👇",
platformOptions: {
platform: "LINKEDIN_PERSON",
visibility: "PUBLIC",
},
},
{
accountId: "soc_tt_…",
platformOptions: {
platform: "TIKTOK_PERSONAL",
privacy: "PUBLIC_TO_EVERYONE",
allowComments: true,
allowDuet: false,
allowStitch: false,
},
},
],
});
Defaults that kick in if you omit platformOptions
- Instagram →
kind: "FEED"
- Facebook →
kind: "FEED"
- X →
replySettings: "everyone"
- LinkedIn (Personal/Company) →
visibility: "PUBLIC"
- YouTube →
visibility: "PUBLIC", madeForKids: false
- Threads →
kind derived from media
The exceptions are TikTok (privacy has no default) and
Pinterest (board has no default) — those two targets require
platformOptions with at least the required field, or the post is
rejected at validation.