Posts

Edit a draft or scheduled post

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.

put/update-post/{postGroupId}

Path parameters

postGroupIdstring required

Headers

x-publora-user-idstring
Idempotency-Keystring

Opt-in idempotency. Omit the header for the previous behaviour — every call applies the update again.

Send a unique value (a UUID is a good choice) per logical operation to make retries safe:

  • Same key + identical body, original request finished → the original status code and response body are replayed. The update is not re-applied.
  • Same key + identical body, original request still in flight → 409 (IDEMPOTENCY_IN_FLIGHT). Retry shortly.
  • Same key + different body → 422 (IDEMPOTENCY_KEY_CONFLICT). Nothing is changed.

The key is bound to this postGroupId as well as the body, so the same key reused against a different post counts as a different request. Keys are scoped to the acting user (the managed user when x-publora-user-id is set), so they never collide across accounts. Records expire 24 hours after they are created; reusing a key after that window is treated as a brand-new request.

Request body

contentstring

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.

platformsstring[]

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

scheduledTimestring 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.

mediaUrlsstring[]

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.

Example request

{
  "platforms": [
    "linkedin-ABC123",
    "twitter-XYZ789"
  ],
  "platformSettings": {
    "instagram": {
      "coverUrl": "https://cdn.example.com/covers/reel-cover.jpg",
      "cover_url": "https://cdn.example.com/covers/reel-cover.jpg"
    },
    "youtube": {
      "playlist": {
        "id": "PLxxxxxxxx",
        "platformId": "youtube-UCxxxxxxxx"
      },
      "thumbnail": {
        "mediaId": "665f...",
        "url": "https://media.publora.com/..."
      }
    },
    "twitter": {
      "replyTo": "https://x.com/customer/status/1234567890123456789",
      "quoteTweet": "987654321098765432"
    }
  }
}

Response

Post updated

successboolean
messagestring
scheduledTimestring 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.

Example response

{
  "message": "Post updated successfully",
  "scheduledTime": "2026-03-01T14:00:00.000Z",
  "mediaValidationStatus": "pending",
  "warnings": [
    {
      "code": "SCHEDULED_TIME_COERCED",
      "message": "Requested scheduled time 2026-03-01T14:00:00.000Z was in the past and was changed to server time 2026-03-01T14:02:11.412Z.",
      "requested": "2026-03-01T14:00:00.000Z",
      "effective": "2026-03-01T14:02:11.412Z",
      "mediaFileId": "507f1f77bcf86cd799439012",
      "mediaStatus": "uploading",
      "pendingCode": "MEDIA_VALIDATION_PENDING",
      "attempts": 3
    }
  ],
  "postGroup": {
    "content": "Corrected launch announcement.",
    "platforms": [
      "linkedin-ABC123",
      "twitter-XYZ789"
    ]
  }
}

Changes