---
title: "Create and schedule a social media post"
method: POST
path: "/create-post"
tags: ["Posts"]
---

# Create and schedule a social media post

`POST /create-post`

Create a post to be published across one or more social media platforms.
Supports text, images, and video (1 video per post).
The image count per post is platform-specific: Twitter/X, Bluesky, and
Mastodon allow up to 4; Instagram, LinkedIn, Facebook, and Telegram
allow up to 10; Threads allows up to 20. There is no separate
"carousel" field — a
carousel is formed implicitly by attaching multiple images to one
postGroupId (Instagram: 2-10 images = carousel, 1 image = single photo),
in upload order. See the Media Uploads guide for the full matrix.
If scheduledTime is provided, the post will be published at that time.
If omitted, the post is saved as a draft.

## Headers

- `x-publora-user-id` string
- `Idempotency-Key` string

## Request body

- object
  - `content` string — Normally required and non-empty. May be omitted or empty only when a targeted LinkedIn connection has repost intent via platformSettings.linkedin.repostEnabled=true or a non-empty repostParentUrn; repost validation still requires a valid parent/setting combination.
  - `platforms` string[], required — Array of platform connection IDs (format: platform-platformId). Each ID must appear at most once; a repeated ID is rejected with 400 "Platforms must not contain duplicates".
  - `scheduledTime` string, date-time — ISO 8601 UTC datetime for scheduling. Omit for draft. If the time is in the past, it is clamped to the current server time and a `SCHEDULED_TIME_COERCED` warning is returned — the post is still created. Read the `scheduledTime` in the response for the time actually stored. Times less than 5 minutes in the past are always tolerated this way. Strict rejection for a time 5 or more minutes in the past is scheduled to begin on **2026-08-25**, unless production configuration overrides that date. Before strict mode it is clamped with a warning. Send a future time to avoid both.
  - `platformSettings` PlatformSettingsInput — Per-platform settings for tiktok, instagram, youtube, threads, twitter, telegram, and linkedin. Unknown platform or nested paths are rejected with 400 PLATFORM_SETTING_UNKNOWN. Merged with defaults on create-post and with existing settings on update-post (omitted fields are preserved).
    - `instagram` object
      - `videoType` 'REELS' | 'STORIES' — How videos are published
      - `coverUrl` string — Custom Reels cover image (alias: cover_url). Publicly accessible http(s) URL to a JPEG — Instagram fetches it server-side at publish time. Empty string clears the custom cover. Invalid URLs are rejected with 400. Ignored for Stories and image posts.
      - `cover_url` string — REST alias for coverUrl; normalized to coverUrl before storage
      - `shareToFeed` boolean — For Reels, false prevents sharing the Reel to the main feed
    - `tiktok` object
      - `viewerSetting` 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY'
      - `allowComments` boolean
      - `allowDuet` boolean
      - `allowStitch` boolean
      - `commercialContent` boolean
      - `brandOrganic` boolean
      - `brandedContent` boolean
    - `youtube` object
      - `privacy` 'private' | 'public' | 'unlisted'
      - `title` string
      - `madeForKids` boolean
      - `tags` union — Video tags. REST accepts an array or a comma-separated string and caps the normalized combined tag budget at 500 characters.
        - string[]
        - string
      - `categoryId` string — Optional YouTube snippet.categoryId
      - `playlist` YouTubePlaylistReference, nullable — Reference to a YouTube playlist for platformSettings.youtube.playlist. Send empty strings for both fields to clear the playlist.
        - `id` string, nullable — YouTube playlist id
        - `platformId` string, nullable — Compound YouTube platform id from platforms; must match the single selected YouTube channel
      - `thumbnail` YouTubeThumbnailReference, nullable — Reference to a Publora-tracked custom thumbnail for platformSettings.youtube.thumbnail. Send an empty url to clear the thumbnail. Set via update-post only (the upload requires a postGroupId).
        - `mediaId` string, nullable — Publora media id returned by the YouTube thumbnail upload endpoint
        - `id` string, nullable — REST alias for mediaId
        - `url` string, nullable — Publora-hosted thumbnail URL returned by the upload endpoint
        - `path` string, nullable — REST alias for url
    - `threads` object
      - `replyControl` 'everyone' | 'accounts_you_follow' | 'mentioned_only' | ''
    - `twitter` object — X reply and quote targets. On self-serve X API tiers, the target author must have mentioned the connected account in that post, quoted one of its posts, or the connected account must have authored the target. Publora cannot prevalidate this relationship; X can reject publication later with X_REPLY_NOT_AUTHORIZED.
      - `replyTo` string — Full x.com/twitter.com status URL or bare 1-19 digit post ID. Publishes the post (or thread head) as a reply. Empty string clears.
      - `quoteTweet` string — Full x.com/twitter.com status URL or bare 1-19 digit post ID. Quotes from the post or thread head; may be combined with replyTo and media. Empty string clears.
    - `telegram` object
      - `disableNotification` boolean
      - `disableWebPagePreview` boolean
      - `protectContent` boolean
    - `linkedin` object — LinkedIn repost (reshare) settings. When a repost is enabled, the group's content becomes the reshare commentary (3,000-char limit) and media is forbidden — scheduling a repost group with media fails validation with code MEDIA_TYPE_NOT_SUPPORTED.
      - `repostEnabled` boolean
      - `repostParentUrn` string — Canonical urn:li:share:* or urn:li:ugcPost:* parent. Required when repostEnabled is true; an empty value means a normal post.
      - `repostVisibility` 'PUBLIC' | 'CONNECTIONS' | '' — Repost visibility. CONNECTIONS is personal-profile-only; company-page reposts must use PUBLIC.
  - `mediaUrls` string[] — Public https URLs downloaded server-side. Images: 25 MB each; videos: 150 MB each; aggregate: 300 MB. Ingestion is all-or-nothing.

## Response `200`

Post created

- object
  - `success` boolean
  - `postGroupId` string
  - `scheduledTime` string, date-time, nullable — The effective scheduled time that was actually stored. Always present. `null` when the post was saved as a draft (no scheduledTime sent). This may differ from the value you sent — if so, a `SCHEDULED_TIME_COERCED` warning explains why. Trust this field over your requested value.
  - `warnings` Warning[] — Non-fatal notices about a request that still succeeded. Only present when at least one warning applies.
    - `code` 'SCHEDULED_TIME_COERCED' | 'MEDIA_VALIDATION_PENDING' — - `SCHEDULED_TIME_COERCED` — the requested `scheduledTime` was in the past and was clamped to server time. Carries `requested` and `effective`. - `MEDIA_VALIDATION_PENDING` — /update-post only. Media validation had not finished, but the post was scheduled anyway; the media is re-checked before publishing. Carries `mediaFileId`, `mediaStatus`, `pendingCode`, and `attempts`. Accompanied by `mediaValidationStatus: "pending"` on the response.
    - `message` string — Human-readable explanation. Do not match on this string — match on code.
    - `requested` string, date-time — SCHEDULED_TIME_COERCED only. The scheduledTime you sent.
    - `effective` string, date-time — SCHEDULED_TIME_COERCED only. The scheduledTime actually stored.
    - `mediaFileId` string, nullable — MEDIA_VALIDATION_PENDING only. The media file still being validated.
    - `mediaStatus` string, nullable — MEDIA_VALIDATION_PENDING only. Status of that media file when the response was sent.
    - `pendingCode` string, nullable — MEDIA_VALIDATION_PENDING only. The last transient probe result.
    - `attempts` integer — MEDIA_VALIDATION_PENDING only. How many validation probes ran before responding.

## Other responses

- `400` — Validation error
- `401` — Invalid API key
- `403` — Access entitlement or plan limit denied; inspect the returned code/error rather than matching an example string
- `409` — An earlier request with this `Idempotency-Key` is still in flight. Nothing was created by this call — retry shortly to receive the original response. Match on `code`; the `error` text varies.
- `422` — This `Idempotency-Key` was already used with a different request body. Nothing was created — either replay the original body, or retry with a new key.
- `429` — Media URL ingestion rate limit (60 URLs per fixed one-hour window)
- `500` — Internal server error

---

[API](https://skmtc.dev/publora/apis/publora-api.md) · [All operations](https://skmtc.dev/publora/apis/publora-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/publora/publora-api/revisions/a1534a88d80a/schema)
