---
title: "Create posts"
method: POST
path: "/posts"
tags: ["Posts"]
---

# Create posts

`POST /posts`

## Request

- Base URL: `https://api.shortsync.app/v1`
- URL: `https://api.shortsync.app/v1/posts`
- Auth: HTTP bearer

Creates one post per `target` entry. A single call can fan out an upload
to multiple platforms; you'll receive one post object per target back,
each with its own status that progresses asynchronously.

### Publish modes

- `immediate` - publish as soon as processing finishes
- `scheduled` - publish at `scheduled_for` (top-level or per-target)
- `draft` - save without publishing; can be promoted via `PATCH /posts/{id}`

### Captions and copy

Top-level `caption` is the default for every non-YouTube target;
per-target `caption` overrides it. `first_comment` works the same
way: top-level is the default, per-target overrides. YouTube targets
ignore `caption` — they read per-target `title` and `description`
instead (no top-level title/description; use `target.title` /
`target.description` if you need them set).

Platform-specific fields like TikTok privacy levels live under
`target.platform_options`.

### Workspace defaults

Anything you leave out of top-level `caption` / `first_comment`,
per-target `caption` / `title` / `description` / `first_comment`,
or `target.platform_options` falls back to the workspace's saved
preferences. This keeps API-created posts in the same "complete
settings" state the dashboard would produce.

Precedence, highest to lowest:

1. `target.<field>` and `target.platform_options.<platform>.<field>`
2. Top-level `caption` / `first_comment`
3. Workspace preferences for this workspace
4. Field stays unset (publish will fail later if the platform
   requires it)

Workspace defaults that get applied today include YouTube
`privacy_status` / `notify_subscribers` / `category_id` /
`default_language` / `playlist_ids` / `tags` /
`upload_early_for_review`, TikTok `privacy_level` / `is_aigc` /
`post_mode`, Instagram `share_to_story` / `collaborators` /
`user_tags` / `prepend_audio_to_caption`, Facebook
`share_to_story`, LinkedIn `visibility`, Discord `silent` /
`mention` / `username` / `threadName`, plus the top-level
`caption` / `first_comment` and the YouTube-only
`title` / `description`. To intentionally publish *without* a
workspace default, send the field explicitly with the value you
want (an empty string clears text fields).

### Partial failure

Request-shape problems (missing required fields, unknown `upload_id`,
invalid `scheduled_for`) return `400` and create no posts.

Per-target operational problems (the connection's token expired, the
upstream platform rejected the post, the upload isn't ready yet) do
**not** fail the whole request. You'll get `201` back with one post
object per target; failed targets carry `status: "failed"` and a
populated `error` object. Always iterate `data` and check each post's
`status` instead of relying solely on the HTTP code.

## Headers

- `Idempotency-Key` string

## Request body

- PostCreate
  - `upload_id` string, required: ID returned from `POST /uploads`. **Required in v1**: text-only posts (no `upload_id`) are reserved for a later release.
  - `publish_mode` 'immediate' | 'scheduled' | 'draft', required
  - `scheduled_for` string, date-time: Required when `publish_mode=scheduled` (unless every target specifies its own `scheduled_for`).
  - `notify_on_success` boolean: Send the org owner an email when publishing succeeds.
  - `caption` string: Default caption applied to every non-YouTube target. Per-target `caption` overrides it. YouTube targets ignore this and read `target.title` + `target.description` instead.
  - `first_comment` string: Default first comment applied to every target that supports first comments: YouTube, Instagram, Facebook, Threads, Bluesky, X, LinkedIn, Mastodon, Discord, and Pixelfed. Per-target `first_comment` overrides it.
  - `targets` PostTarget[], required
    - `connection_id` string, required
    - `caption` string: Per-target caption. Falls back to the top-level `caption`. Ignored for YouTube targets — YouTube uses `title` and `description`.
    - `title` string: Sets the post title, used by YouTube, Pinterest and Tumblr (where it's an optional heading).
    - `description` string: Sets the long-form description, used by YouTube, Pinterest and Tumblr (where it's the post text).
    - `first_comment` string
    - `first_comment_enabled` boolean: Set `false` to skip the first comment on this target even when a top-level `first_comment` is set. Omit to use the user's saved default (on, unless turned off for this platform in the dashboard).
    - `scheduled_for` string, date-time: Per-target schedule override. Falls back to top-level `scheduled_for`.
    - `platform_options` PlatformOptions: Per-platform overrides. Keys are platform identifiers; values are platform-specific objects. Only the keys matching the target's platform are read.
      - `tiktok` TikTokOptions
        - `post_mode` 'direct' | 'draft'
        - `privacy_level` string: TikTok privacy level (e.g. `PUBLIC_TO_EVERYONE`).
        - `disable_comment` boolean
        - `disable_duet` boolean
        - `disable_stitch` boolean
        - `commercial_content_disclosure` boolean: Turns on TikTok's commercial content disclosure. Must be true for `is_branded_content` and `is_your_brand` to take effect.
        - `is_branded_content` boolean
        - `is_your_brand` boolean
        - `is_aigc` boolean
        - `auto_add_music` boolean: Photo/carousel posts only. When true, TikTok auto-picks and attaches background music. Ignored for video uploads.
      - `youtube` YouTubeOptions
        - `privacy_status` 'public' | 'unlisted' | 'private'
        - `made_for_kids` boolean: Required when the video is primarily directed at children. Setting to `true` disables comments, personalized ads, and forces `notify_subscribers` to false (YouTube doesn't allow subscriber notifications on kid-directed content).
        - `notify_subscribers` boolean
        - `category_id` string
        - `default_language` string
        - `tags` string[]
        - `playlist_ids` string[]
        - `upload_early_for_review` boolean: Upload as private now and flip to public at `scheduled_for`.
        - `contains_synthetic_media` boolean: YouTube's "Altered content / AI use" disclosure. Enable when the video contains synthetic, altered, or AI-generated content that could be mistaken for a real place, person, or event. Maps to `status.containsSyntheticMedia`.
        - `recording_date` string, date: Date the video was recorded, as `YYYY-MM-DD`. Surfaces in YouTube Studio under "Recording date and location". Maps to `recordingDetails.recordingDate` (pinned to midnight UTC).
        - `has_paid_product_placement` boolean: YouTube's paid promotion disclosure. Enable when the video contains paid product placement or sponsorship. YouTube shows an "Includes paid promotion" label to viewers. Maps to `paidProductPlacementDetails.hasPaidProductPlacement`.
      - `instagram` InstagramOptions
        - `share_to_feed` boolean: Reel also appears in the main feed. Mutually exclusive with `story_only`.
        - `share_to_story` boolean: Cross-post a Story alongside the Reel. Mutually exclusive with `story_only`.
        - `story_only` boolean: Publish only as a Story (no Reel). Mutually exclusive with `share_to_feed` and `share_to_story`.
        - `trial_reel` boolean
        - `trial_graduation_strategy` 'MANUAL' | 'SS_PERFORMANCE'
        - `audio_name` string
        - `collaborators` string[]
        - `user_tags` string[]
        - `location_id` string: Facebook Page ID of the location to tag.
        - `is_paid_partnership` boolean: Shows the "Paid partnership" label.
        - `is_ai_generated` boolean: Adds Instagram's "AI info" label to the post.
      - `facebook` FacebookOptions
        - `share_to_story` boolean: Also post as a Story alongside the Reel. Mutually exclusive with `story_only`.
        - `story_only` boolean: Publish only as a Story (no Reel). Mutually exclusive with `share_to_story`.
        - `is_ai_generated` boolean: Adds Facebook's "AI info" label to the Reel, photo or carousel. Not applied to Stories or text posts.
      - `twitter` TwitterOptions
        - `reply_settings` 'everyone' | 'following' | 'mentionedUsers' | 'subscribers' | 'verified'
        - `community_id` string
        - `text_only` boolean
        - `for_super_followers_only` boolean
      - `threads` ThreadsOptions
        - `text_only` boolean
      - `bluesky` BlueskyOptions
        - `langs` string[]
        - `text_only` boolean
      - `snapchat` SnapchatOptions
        - `formats` string[]: Which Snapchat surfaces to publish to. `spotlight` ships to the Spotlight discovery feed; `story` is a 24-hour broadcast to your followers. Pass both to publish to both at once. Defaults to `["spotlight"]`. Hashtags aren't a separate field on Snapchat — include them inline in the caption. Snapchat rejects Spotlight captions over 160 characters with `SNAPCHAT_DESCRIPTION_TOO_LONG`. Story doesn't use the caption at all.
        - `skip_save_to_profile` boolean: Spotlight-only. When `true`, ask Snapchat not to save the Spotlight to your Public Profile grid; the video still ships to the Spotlight discovery feed. Ignored when `formats` doesn't include `spotlight`.
      - `pinterest` PinterestOptions
        - `board_id` string, required: Destination Pinterest board ID. Required unless the workspace has set a default board (`video_preferences.pinterest_board_id`), in which case the default is used when omitted. Fetch board IDs via `GET /api/v1/platforms/pinterest/boards`.
        - `link` string, uri: Outbound destination URL for the pin. When set, tapping the pin on Pinterest opens this URL. Optional.
        - `alt_text` string: Accessibility alt text for the pin image or video cover. Pinterest caps this at 500 characters; longer values are truncated server-side.
        - `title` string: Override for the pin title. When omitted, falls back to the workspace title, then to a truncated slice of the caption. Pinterest caps titles at 100 characters. The pin's description comes from the top-level `caption` on the post (or the `platform_captions.pinterest` override).
      - `linkedin` LinkedInOptions
        - `visibility` 'PUBLIC' | 'CONNECTIONS' | 'LOGGED_IN': Who can see the post. Company Page authors always post as `PUBLIC`; LinkedIn doesn't support the other values for Pages.
      - `telegram` TelegramOptions
        - `text_only` boolean: Send a text-only message (no media). The upload's bytes are skipped. Ignored for image uploads.
      - `mastodon` MastodonOptions
        - `visibility` 'public' | 'unlisted' | 'private': `public` appears on public timelines, `unlisted` is federated but hidden from public timelines, `private` is followers-only.
        - `language` string: BCP-47 language code (e.g. `en`, `fi`).
        - `text_only` boolean: Publish a text-only post (no media).
      - `discord` DiscordOptions: A workspace can connect several Discord channels (one webhook each). A post goes to the channels in `channel_ids`, or to every channel with `is_default: true` when it's omitted. Caption max 2000 characters, up to 10 attachments and 10 MB per post. No custom thumbnail. `first_comment` (max 2000 characters) is posted as a separate message right after the post in text channels (webhooks can't reply there) and as the first reply inside the new post in forum channels. It never sends its own notification, and a failed comment never fails the post. Saved per-user defaults for `silent`, `mention` and `username` apply when you leave them out.
        - `channel_ids` string[]: Channels to post to, by `id` from `GET /api/v1/discord/channels`. Omit to use the workspace's default channels. Ids that are no longer connected are skipped; if none are left the publish fails with `DISCORD_CHANNELS_NOT_FOUND`.
        - `silent` boolean: Send without push or desktop notifications.
        - `mention` 'none' | 'here' | 'everyone': Ping to add to the message (`@here` or `@everyone`).
        - `thread_name` string: Forum post title. Forum channels require one; when omitted, the first line of the caption is used. Ignored by regular text channels.
        - `username` string: Overrides the webhook's display name for this post. Discord rejects names containing "discord", so they're refused with `INVALID_PLATFORM_OPTIONS`.
        - `text_only` boolean: Send only the caption as a plain message (no media). Applies to video uploads; ignored for image and carousel uploads.
      - `tumblr` TumblrOptions: Tumblr is in beta. One Tumblr login can own several blogs; a post goes to the blog in `blog_name`, or to the account's primary blog when it's omitted. The post text is the post's `description` (up to 5,000 characters, split into paragraphs); when no description is set, the caption is used instead. A per-target `caption` on the Tumblr target wins over both. Video posts take one video (up to 500 MB and 10 minutes); photo posts take up to 10 images (no video mix); text posts need post text or a `title`. No custom thumbnail and no first comment. Tumblr limits each account to 250 posts and 20 videos (60 minutes of video) per day.
        - `blog_name` string: Short name of the blog to post to (e.g. `myblog` for myblog.tumblr.com), one of the connection's blogs. Omit to use the primary blog. A blog that's no longer on the connected account fails the publish with `TUMBLR_BLOG_NOT_FOUND`.
        - `tags` string[]: Tags without the leading "#". Commas aren't allowed inside a tag. Duplicates are dropped.
        - `title` string: Optional heading shown above the post text. Overrides the post's shared `title`.
        - `text_only` boolean: Post only the title and post text as a text post (no media). Applies to video uploads; ignored for image and carousel uploads.
      - `pixelfed` PixelfedOptions: Pixelfed is in beta. It's a federated, Mastodon-compatible photo network; each connection is one account on one server (instance). Every post needs media (video, image or carousel); there are no text-only posts. Carousels can mix photos and videos, up to 20 items, but the connected server's own album cap (often 4) is checked at publish. Caption max 500 characters (some servers allow more). Photos and videos share one file-size cap set by the server (default about 15 MB); oversized videos are compressed, oversized images fail. Video is off by default on Pixelfed servers, so posting video to a photo-only server fails with a clear error. `first_comment` (max 500 characters) is posted as a reply under the post. No custom thumbnail. Saved workspace defaults for `visibility` and `sensitive` apply when you leave them out.
        - `visibility` 'public' | 'unlisted' | 'private': `public` shows the post on public timelines, `unlisted` hides it from them, `private` shows it to followers only.
        - `sensitive` boolean: Blurs the media behind a warning.
        - `spoiler_text` string: Content-warning text shown over the post. Only used when `sensitive` is true.
      - `google_business` GoogleBusinessOptions: Google Business Profile is in beta. One Google login can manage several business locations; a post goes to the locations in `location_ids`, or to every location with `is_default: true` when it's omitted (the first postable location if none is marked), and is published as an update on each. Caption (required) max 1500 characters. One photo (JPG or PNG, 10 KB to 5 MB) or text only: no video, no carousel, no custom thumbnail, no first comment. Saved per-user defaults for `cta_type` and `cta_url` apply when you leave them out.
        - `location_ids` string[]: Locations to post to, by `id` from `GET /api/v1/google-business/locations`. Omit to use the default locations. An id that's no longer on the connected account fails the publish with `GOOGLE_BUSINESS_LOCATION_NOT_FOUND`.
        - `cta_type` 'BOOK' | 'ORDER' | 'SHOP' | 'LEARN_MORE' | 'SIGN_UP' | 'CALL', nullable: Optional button on the post. `CALL` dials the profile's phone number and takes no URL; every other type needs `cta_url`. `null` removes the button.
        - `cta_url` string, uri, nullable: The button's link (http or https). Required with every `cta_type` except `CALL`.
        - `text_only` boolean: Post only the caption (no media). Google Business Profile can't post video, so a video upload needs this. Ignored for image uploads.
  - `custom_thumbnail_url` string: Public `https://` URL to a cover image (jpeg, png, webp, or gif, max 20 MB). ShortSync downloads and stores it. Send an empty string to clear. Mutually exclusive with `thumbnail_timestamp_ms` and `custom_thumbnail_upload_id`.
  - `thumbnail_timestamp_ms` integer: Extract a frame from the video at this timestamp (milliseconds) and use it as the cover. Video-only. Mutually exclusive with `custom_thumbnail_url` and `custom_thumbnail_upload_id`.
  - `custom_thumbnail_upload_id` string: `upload_id` from `POST /uploads` whose bytes are an image. ShortSync copies the image into the post's thumbnail slot and then deletes the source upload. Mutually exclusive with `custom_thumbnail_url` and `thumbnail_timestamp_ms`.

## Response `201`

Posts created. Each target produced one post. Some posts may carry
`status: "failed"` if an operational issue prevented them, so inspect
each entry in `data`.

- object
  - `data` Post[], required
    - `id` string, required
    - `workspace_id` string, required
    - `upload_id` string, nullable
    - `platform` 'instagram' | 'tiktok' | 'youtube' | 'facebook' | 'snapchat' | 'pinterest' | 'threads' | 'twitter' | 'bluesky' | 'linkedin' | 'reddit' | 'telegram' | 'mastodon' | 'discord' | 'tumblr' | 'pixelfed' | 'google_business', required
    - `connection_id` string, required
    - `status` 'processing' | 'ready' | 'scheduled' | 'published' | 'failed' | 'deleted' | 'skipped_inactive_subscription', required
    - `publish_mode` 'immediate' | 'scheduled' | 'draft', required
    - `scheduled_for` string, date-time, nullable
    - `published_at` string, date-time, nullable
    - `platform_url` string, uri, nullable: Public URL of the published post, when available.
    - `caption` string, nullable
    - `title` string, nullable
    - `description` string, nullable
    - `first_comment` string, nullable
    - `platform_options` PlatformOptions: Per-platform overrides. Keys are platform identifiers; values are platform-specific objects. Only the keys matching the target's platform are read.
      - `tiktok` TikTokOptions
        - `post_mode` 'direct' | 'draft'
        - `privacy_level` string: TikTok privacy level (e.g. `PUBLIC_TO_EVERYONE`).
        - `disable_comment` boolean
        - `disable_duet` boolean
        - `disable_stitch` boolean
        - `commercial_content_disclosure` boolean: Turns on TikTok's commercial content disclosure. Must be true for `is_branded_content` and `is_your_brand` to take effect.
        - `is_branded_content` boolean
        - `is_your_brand` boolean
        - `is_aigc` boolean
        - `auto_add_music` boolean: Photo/carousel posts only. When true, TikTok auto-picks and attaches background music. Ignored for video uploads.
      - `youtube` YouTubeOptions
        - `privacy_status` 'public' | 'unlisted' | 'private'
        - `made_for_kids` boolean: Required when the video is primarily directed at children. Setting to `true` disables comments, personalized ads, and forces `notify_subscribers` to false (YouTube doesn't allow subscriber notifications on kid-directed content).
        - `notify_subscribers` boolean
        - `category_id` string
        - `default_language` string
        - `tags` string[]
        - `playlist_ids` string[]
        - `upload_early_for_review` boolean: Upload as private now and flip to public at `scheduled_for`.
        - `contains_synthetic_media` boolean: YouTube's "Altered content / AI use" disclosure. Enable when the video contains synthetic, altered, or AI-generated content that could be mistaken for a real place, person, or event. Maps to `status.containsSyntheticMedia`.
        - `recording_date` string, date: Date the video was recorded, as `YYYY-MM-DD`. Surfaces in YouTube Studio under "Recording date and location". Maps to `recordingDetails.recordingDate` (pinned to midnight UTC).
        - `has_paid_product_placement` boolean: YouTube's paid promotion disclosure. Enable when the video contains paid product placement or sponsorship. YouTube shows an "Includes paid promotion" label to viewers. Maps to `paidProductPlacementDetails.hasPaidProductPlacement`.
      - `instagram` InstagramOptions
        - `share_to_feed` boolean: Reel also appears in the main feed. Mutually exclusive with `story_only`.
        - `share_to_story` boolean: Cross-post a Story alongside the Reel. Mutually exclusive with `story_only`.
        - `story_only` boolean: Publish only as a Story (no Reel). Mutually exclusive with `share_to_feed` and `share_to_story`.
        - `trial_reel` boolean
        - `trial_graduation_strategy` 'MANUAL' | 'SS_PERFORMANCE'
        - `audio_name` string
        - `collaborators` string[]
        - `user_tags` string[]
        - `location_id` string: Facebook Page ID of the location to tag.
        - `is_paid_partnership` boolean: Shows the "Paid partnership" label.
        - `is_ai_generated` boolean: Adds Instagram's "AI info" label to the post.
      - `facebook` FacebookOptions
        - `share_to_story` boolean: Also post as a Story alongside the Reel. Mutually exclusive with `story_only`.
        - `story_only` boolean: Publish only as a Story (no Reel). Mutually exclusive with `share_to_story`.
        - `is_ai_generated` boolean: Adds Facebook's "AI info" label to the Reel, photo or carousel. Not applied to Stories or text posts.
      - `twitter` TwitterOptions
        - `reply_settings` 'everyone' | 'following' | 'mentionedUsers' | 'subscribers' | 'verified'
        - `community_id` string
        - `text_only` boolean
        - `for_super_followers_only` boolean
      - `threads` ThreadsOptions
        - `text_only` boolean
      - `bluesky` BlueskyOptions
        - `langs` string[]
        - `text_only` boolean
      - `snapchat` SnapchatOptions
        - `formats` string[]: Which Snapchat surfaces to publish to. `spotlight` ships to the Spotlight discovery feed; `story` is a 24-hour broadcast to your followers. Pass both to publish to both at once. Defaults to `["spotlight"]`. Hashtags aren't a separate field on Snapchat — include them inline in the caption. Snapchat rejects Spotlight captions over 160 characters with `SNAPCHAT_DESCRIPTION_TOO_LONG`. Story doesn't use the caption at all.
        - `skip_save_to_profile` boolean: Spotlight-only. When `true`, ask Snapchat not to save the Spotlight to your Public Profile grid; the video still ships to the Spotlight discovery feed. Ignored when `formats` doesn't include `spotlight`.
      - `pinterest` PinterestOptions
        - `board_id` string, required: Destination Pinterest board ID. Required unless the workspace has set a default board (`video_preferences.pinterest_board_id`), in which case the default is used when omitted. Fetch board IDs via `GET /api/v1/platforms/pinterest/boards`.
        - `link` string, uri: Outbound destination URL for the pin. When set, tapping the pin on Pinterest opens this URL. Optional.
        - `alt_text` string: Accessibility alt text for the pin image or video cover. Pinterest caps this at 500 characters; longer values are truncated server-side.
        - `title` string: Override for the pin title. When omitted, falls back to the workspace title, then to a truncated slice of the caption. Pinterest caps titles at 100 characters. The pin's description comes from the top-level `caption` on the post (or the `platform_captions.pinterest` override).
      - `linkedin` LinkedInOptions
        - `visibility` 'PUBLIC' | 'CONNECTIONS' | 'LOGGED_IN': Who can see the post. Company Page authors always post as `PUBLIC`; LinkedIn doesn't support the other values for Pages.
      - `telegram` TelegramOptions
        - `text_only` boolean: Send a text-only message (no media). The upload's bytes are skipped. Ignored for image uploads.
      - `mastodon` MastodonOptions
        - `visibility` 'public' | 'unlisted' | 'private': `public` appears on public timelines, `unlisted` is federated but hidden from public timelines, `private` is followers-only.
        - `language` string: BCP-47 language code (e.g. `en`, `fi`).
        - `text_only` boolean: Publish a text-only post (no media).
      - `discord` DiscordOptions: A workspace can connect several Discord channels (one webhook each). A post goes to the channels in `channel_ids`, or to every channel with `is_default: true` when it's omitted. Caption max 2000 characters, up to 10 attachments and 10 MB per post. No custom thumbnail. `first_comment` (max 2000 characters) is posted as a separate message right after the post in text channels (webhooks can't reply there) and as the first reply inside the new post in forum channels. It never sends its own notification, and a failed comment never fails the post. Saved per-user defaults for `silent`, `mention` and `username` apply when you leave them out.
        - `channel_ids` string[]: Channels to post to, by `id` from `GET /api/v1/discord/channels`. Omit to use the workspace's default channels. Ids that are no longer connected are skipped; if none are left the publish fails with `DISCORD_CHANNELS_NOT_FOUND`.
        - `silent` boolean: Send without push or desktop notifications.
        - `mention` 'none' | 'here' | 'everyone': Ping to add to the message (`@here` or `@everyone`).
        - `thread_name` string: Forum post title. Forum channels require one; when omitted, the first line of the caption is used. Ignored by regular text channels.
        - `username` string: Overrides the webhook's display name for this post. Discord rejects names containing "discord", so they're refused with `INVALID_PLATFORM_OPTIONS`.
        - `text_only` boolean: Send only the caption as a plain message (no media). Applies to video uploads; ignored for image and carousel uploads.
      - `tumblr` TumblrOptions: Tumblr is in beta. One Tumblr login can own several blogs; a post goes to the blog in `blog_name`, or to the account's primary blog when it's omitted. The post text is the post's `description` (up to 5,000 characters, split into paragraphs); when no description is set, the caption is used instead. A per-target `caption` on the Tumblr target wins over both. Video posts take one video (up to 500 MB and 10 minutes); photo posts take up to 10 images (no video mix); text posts need post text or a `title`. No custom thumbnail and no first comment. Tumblr limits each account to 250 posts and 20 videos (60 minutes of video) per day.
        - `blog_name` string: Short name of the blog to post to (e.g. `myblog` for myblog.tumblr.com), one of the connection's blogs. Omit to use the primary blog. A blog that's no longer on the connected account fails the publish with `TUMBLR_BLOG_NOT_FOUND`.
        - `tags` string[]: Tags without the leading "#". Commas aren't allowed inside a tag. Duplicates are dropped.
        - `title` string: Optional heading shown above the post text. Overrides the post's shared `title`.
        - `text_only` boolean: Post only the title and post text as a text post (no media). Applies to video uploads; ignored for image and carousel uploads.
      - `pixelfed` PixelfedOptions: Pixelfed is in beta. It's a federated, Mastodon-compatible photo network; each connection is one account on one server (instance). Every post needs media (video, image or carousel); there are no text-only posts. Carousels can mix photos and videos, up to 20 items, but the connected server's own album cap (often 4) is checked at publish. Caption max 500 characters (some servers allow more). Photos and videos share one file-size cap set by the server (default about 15 MB); oversized videos are compressed, oversized images fail. Video is off by default on Pixelfed servers, so posting video to a photo-only server fails with a clear error. `first_comment` (max 500 characters) is posted as a reply under the post. No custom thumbnail. Saved workspace defaults for `visibility` and `sensitive` apply when you leave them out.
        - `visibility` 'public' | 'unlisted' | 'private': `public` shows the post on public timelines, `unlisted` hides it from them, `private` shows it to followers only.
        - `sensitive` boolean: Blurs the media behind a warning.
        - `spoiler_text` string: Content-warning text shown over the post. Only used when `sensitive` is true.
      - `google_business` GoogleBusinessOptions: Google Business Profile is in beta. One Google login can manage several business locations; a post goes to the locations in `location_ids`, or to every location with `is_default: true` when it's omitted (the first postable location if none is marked), and is published as an update on each. Caption (required) max 1500 characters. One photo (JPG or PNG, 10 KB to 5 MB) or text only: no video, no carousel, no custom thumbnail, no first comment. Saved per-user defaults for `cta_type` and `cta_url` apply when you leave them out.
        - `location_ids` string[]: Locations to post to, by `id` from `GET /api/v1/google-business/locations`. Omit to use the default locations. An id that's no longer on the connected account fails the publish with `GOOGLE_BUSINESS_LOCATION_NOT_FOUND`.
        - `cta_type` 'BOOK' | 'ORDER' | 'SHOP' | 'LEARN_MORE' | 'SIGN_UP' | 'CALL', nullable: Optional button on the post. `CALL` dials the profile's phone number and takes no URL; every other type needs `cta_url`. `null` removes the button.
        - `cta_url` string, uri, nullable: The button's link (http or https). Required with every `cta_type` except `CALL`.
        - `text_only` boolean: Post only the caption (no media). Google Business Profile can't post video, so a video upload needs this. Ignored for image uploads.
    - `error` object, nullable
      - `code` string
      - `message` string
      - `details` unknown
    - `created_at` string, date-time, required
    - `updated_at` string, date-time

## Other responses

- `400`: Request body or parameters failed validation.
- `401`: Missing, malformed, or revoked token.
- `403`: Token is valid but cannot access this resource. Common cases: - `INSUFFICIENT_SCOPE` - token lacks the required scope. - `WORKSPACE_NOT_ALLOWED` - account token is allowlisted to other workspaces.
- `429`: Per-key sliding-window rate limit (60 req/min), per-org monthly request quota, or per-user upload cap was hit. `RATE_LIMIT_EXCEEDED` carries the window headers; `QUOTA_EXCEEDED` carries `details.quota` (which counter), `used`, `limit`, `resets_at`, and `upgrade_url`. `UPLOAD_QUOTA_EXCEEDED` (Free tier on `POST /posts` only) carries the same shape plus `details.remaining`.

---

[API](https://skmtc.dev/shortsync/apis/shortsync-api.md) · [All operations](https://skmtc.dev/shortsync/apis/shortsync-api/llms.txt) · [OpenAPI document](https://skmtc.dev/shortsync/apis/shortsync-api/revisions/71fdaef1e400?raw)
