---
title: "Edit a draft or scheduled post"
method: PUT
path: "/update-post/{postGroupId}"
tags: ["Posts"]
---

# Edit a draft or scheduled post

`PUT /update-post/{postGroupId}`

Modify the content, target platforms, scheduled time, status, platformSettings,
or mediaUrls of an existing draft or scheduled post.
Only post groups whose status is draft or scheduled can be updated. `pending`
and `processing` belong to separate processing/per-platform state, not
to the post-group status enum.
At least one of status, scheduledTime, content, platforms, platformSettings, or
mediaUrls must be provided. Every field is a patch — omit it to leave the stored
value unchanged.
`content` replaces the base text and rewrites each platform post to its effective
content, preserving explicit per-account overrides. `platforms` replaces the whole
target set (it is not merged); added connections are validated for ownership and
plan entitlement, and adding a target to a scheduled post re-runs scheduling
quota and post validation.
platformSettings are merged per-platform with existing settings —
omitted fields are preserved.
Updating a post group also updates all associated platform-specific posts.
A content or platform edit is refused with POST_NOT_EDITABLE (400) or
POST_PUBLISH_IN_PROGRESS / POST_GROUP_VERSION_CONFLICT (409) rather than
partially applied.

## Path parameters

- `postGroupId` string, required

## Headers

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

## Request body

- object — At least one of status, scheduledTime, content, platforms, platformSettings, or mediaUrls must be provided
  - `content` string — Replacement base post text. Every platform post without an explicit per-account override is rewritten to this text; overrides created in the web editor are preserved. Editing the text of a Twitter or Threads target clears its derived thread split so it is recomputed. An empty string is accepted while the post stays a draft. Scheduling still enforces each platform's content and media rules.
  - `platforms` string[] — Replacement target set. The array REPLACES the stored one — it is not merged, so send the complete final list. Use the exact connection IDs returned by GET /platform-connections; duplicates are rejected with INVALID_PLATFORMS and unknown/foreign IDs with INVALID_PLATFORM_CONNECTION. IDs removed from the array have their platform posts deleted. An empty array is accepted only while the post remains a draft; scheduling an empty set returns PLATFORMS_REQUIRED.
  - `status` 'draft' | 'scheduled' — New status
  - `scheduledTime` string, date-time — New ISO 8601 UTC scheduled time. Send a future time. If the time is in the past, it is clamped to the current server time and a `SCHEDULED_TIME_COERCED` warning is returned — the update still succeeds. 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. The same check applies to the post's existing scheduledTime when you move a post to `status: "scheduled"` without sending a new time — a long-stale draft can therefore be rejected.
  - `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 and appended to existing media. Images: 25 MB each; videos: 150 MB each; aggregate: 300 MB. Ingestion is all-or-nothing.

## Response `200`

Post updated

- object
  - `success` boolean
  - `message` string
  - `scheduledTime` string, date-time, nullable — The effective scheduled time that was actually stored. Always present. `null` when the post has no scheduled time (e.g. it was moved back to draft). This may differ from the value you sent — if so, a `SCHEDULED_TIME_COERCED` warning explains why. Trust this field over your requested value.
  - `mediaValidationStatus` 'pending' — Only present when the post was scheduled while media validation was still unfinished. The media is re-checked before publishing. Accompanied by a `MEDIA_VALIDATION_PENDING` entry in `warnings`.
  - `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.
  - `postGroup` object
    - `_id` string
    - `status` string
    - `content` string — The effective base text stored after the update. Always present; an empty string when the post has no base text.
    - `platforms` string[] — The effective target set stored after the update. Always present; an empty array for a draft with no targets.
    - `scheduledTime` string, date-time — Only included if the post has a scheduled time. Duplicates the top-level `scheduledTime`, which is always present and is the preferred field to read.

## Other responses

- `400` — Invalid request
- `401` — Invalid API key
- `404` — Post not found
- `409` — The update conflicts with concurrent work and nothing was changed. Either an earlier request with this `Idempotency-Key` is still in flight, or the post is being published, or another write committed first. Match on `code`; the `error` text varies. Re-read the post with GET /get-post before retrying — do not blind-retry an edit that races publishing.
- `422` — This `Idempotency-Key` was already used with a different request body (or against a different postGroupId). Nothing was changed — either replay the original body, or retry with a new key.
- `429` — Media URL ingestion rate limit reached (60 URLs per fixed one-hour window)
- `500` — 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)
