---
title: "Update post"
method: PUT
path: "/v1/posts/{postId}"
tags: ["Posts"]
---

# Update post

`PUT /v1/posts/{postId}`

Update an existing post. Draft, scheduled, failed, partial, and cancelled posts can be edited.
Published posts can only have their recycling config updated.

To promote a draft to scheduled, send `isDraft: false` together with `scheduledFor` (or `publishNow: true`,
or `queuedFromProfile`). If `isDraft` is omitted the post keeps its current draft status, so sending only
`scheduledFor` to a draft returns 200 but the post remains a draft.

Non-draft updates run the same per-platform validation as post creation (media requirements, platform-specific
field rules, etc.) against the resulting platforms, returning 400 on failure.

## Path parameters

- `postId` string, required

## Request body

- object
  - `title` string — Stored on the post for reference/display only. This field is NOT used as the video title when publishing. To set a YouTube video title, use platformSpecificData.title on the youtube platform target (falls back to the first line of content when omitted).
  - `content` string
  - `mediaItems` MediaItem[]
    - `type` 'image' | 'video' | 'gif' | 'document'
    - `url` string, uri — A media item with a null, missing or empty url is dropped.
    - `title` string — Optional title for the media item. Used as the document title for LinkedIn PDF/carousel posts. If omitted, falls back to the post title, then the filename.
    - `altText` string — Accessibility alternative text for an image, applied on every platform that supports it: Instagram (feed images only, not Reels/Stories), Facebook, Threads, X (max 1000 chars), LinkedIn, Bluesky, and Pinterest (max 500 chars). Ignored on platforms without alt-text support (TikTok, YouTube, Snapchat, Telegram, Reddit, Google Business Profile, WhatsApp) and on video items where the platform does not accept it. Set once per image; the same value is sent to each selected platform.
    - `filename` string
    - `size` integer — Optional file size in bytes
    - `mimeType` string — Optional MIME type (e.g. image/jpeg, video/mp4)
    - `thumbnail` string, uri — Optional custom thumbnail/cover image URL for videos. Supported for Facebook video posts, Facebook Reels, regular video uploads, and LinkedIn video posts. Max 10MB, JPG/PNG recommended.
    - `instagramThumbnail` string, uri — Custom cover image URL for Instagram Reels. Can also be set via platformSpecificData.instagramThumbnail or platformSpecificData.reelCover. Resolution order: this field > platformSpecificData.instagramThumbnail > platformSpecificData.reelCover > platformSpecificData.thumbnailUrl (legacy).
    - `tiktokProcessed` boolean — Internal flag indicating the image was resized for TikTok
  - `platforms` object[] — Target platforms and accounts for this post. Each item must include platform and accountId.
    - `platform` string, required
    - `accountId` string, required
    - `customContent` string — Platform-specific text override.
    - `customMedia` MediaItem[]
      - `type` 'image' | 'video' | 'gif' | 'document'
      - `url` string, uri — A media item with a null, missing or empty url is dropped.
      - `title` string — Optional title for the media item. Used as the document title for LinkedIn PDF/carousel posts. If omitted, falls back to the post title, then the filename.
      - `altText` string — Accessibility alternative text for an image, applied on every platform that supports it: Instagram (feed images only, not Reels/Stories), Facebook, Threads, X (max 1000 chars), LinkedIn, Bluesky, and Pinterest (max 500 chars). Ignored on platforms without alt-text support (TikTok, YouTube, Snapchat, Telegram, Reddit, Google Business Profile, WhatsApp) and on video items where the platform does not accept it. Set once per image; the same value is sent to each selected platform.
      - `filename` string
      - `size` integer — Optional file size in bytes
      - `mimeType` string — Optional MIME type (e.g. image/jpeg, video/mp4)
      - `thumbnail` string, uri — Optional custom thumbnail/cover image URL for videos. Supported for Facebook video posts, Facebook Reels, regular video uploads, and LinkedIn video posts. Max 10MB, JPG/PNG recommended.
      - `instagramThumbnail` string, uri — Custom cover image URL for Instagram Reels. Can also be set via platformSpecificData.instagramThumbnail or platformSpecificData.reelCover. Resolution order: this field > platformSpecificData.instagramThumbnail > platformSpecificData.reelCover > platformSpecificData.thumbnailUrl (legacy).
      - `tiktokProcessed` boolean — Internal flag indicating the image was resized for TikTok
    - `scheduledFor` string, date-time — Optional per-platform scheduled time override.
    - `platformSpecificData` object — A <platform>Settings namespace (e.g. facebookSettings, tiktokSettings) omitted from the request is preserved from the stored post. Sending the key replaces the whole namespace; it is not deep-merged.
  - `scheduledFor` string, date-time
  - `publishNow` boolean
  - `isDraft` boolean — When omitted, the post keeps its current draft status. Send `false` to promote a draft to scheduled (combined with `scheduledFor`, `publishNow`, or a queue).
  - `timezone` string
  - `visibility` 'public' | 'private' | 'unlisted'
  - `tags` string[]
  - `hashtags` string[] — Stored for reference only. Hashtags are NOT automatically appended to the caption when publishing. Include hashtags directly in the content field (platforms like Instagram only support hashtags as caption text). For YouTube keywords, use the tags field instead.
  - `mentions` string[]
  - `crosspostingEnabled` boolean
  - `metadata` object
  - `queuedFromProfile` string — Profile ID to schedule via queue.
  - `queueId` string — Specific queue ID to use when scheduling via queue.
  - `tiktokSettings` TikTokPlatformData — Photo carousels up to 35 images. Video titles up to 2200 chars, photo titles truncated to 90 chars. privacyLevel must match creator_info options. Both camelCase and snake_case accepted. Creator Inbox (draft mode): Set draft: true to send content to the TikTok Creator Inbox instead of publishing immediately. The creator receives an inbox notification and completes the post using TikTok's editing flow. This maps to TikTok's post_mode: "MEDIA_UPLOAD" internally. Important: The field publish_type is NOT supported. Use draft: true for Creator Inbox flow. Photo drafts use the /v2/post/publish/content/init/ endpoint with post_mode: "MEDIA_UPLOAD". Video drafts use the dedicated /v2/post/publish/inbox/video/init/ endpoint. When draft: true, the video.upload scope is required. When draft is false or omitted (direct post), the video.publish scope is required. For Creator Inbox, TikTok app version must be 31.8 or higher.
    - `draft` boolean — When true, sends the post to the TikTok Creator Inbox as a draft instead of publishing immediately. The creator receives an inbox notification to complete posting via TikTok's editing flow. Maps to TikTok API post_mode: "MEDIA_UPLOAD" (photos) or the dedicated inbox endpoint (videos). When false or omitted, publishes directly via post_mode: "DIRECT_POST". Note: publish_type is not a supported field. Use this field instead.
    - `privacyLevel` string — One of the values returned by the TikTok creator info API for the account. Accounts connected through the TikTok for Business app publish videos as public only: a non-public value on a video post is rejected at creation unless draft is true (photo posts keep every level).
    - `allowComment` boolean — Allow comments on the post
    - `allowDuet` boolean — Allow duets (required for video posts)
    - `allowStitch` boolean — Allow stitches (required for video posts)
    - `commercialContentType` 'none' | 'brand_organic' | 'brand_content' — Type of commercial content disclosure. Sufficient on its own: "brand_organic" ("Your Brand") implies isBrandOrganicPost and "brand_content" ("Branded Content", paid partnership) implies brandPartnerPromote, so you don't need to send the boolean flags separately. Branded content cannot be posted with privacyLevel SELF_ONLY.
    - `brandPartnerPromote` boolean — Whether the post promotes a brand partner (branded content / paid partnership). Only needed to disclose BOTH types at once (set it alongside commercialContentType "brand_organic"), or to override the value implied by commercialContentType.
    - `isBrandOrganicPost` boolean — Whether the post promotes the creator's own brand (brand organic). Only needed to disclose BOTH types at once (set it alongside commercialContentType "brand_content"), or to override the value implied by commercialContentType.
    - `contentPreviewConfirmed` boolean — User has confirmed they previewed the content
    - `expressConsentGiven` boolean — User has given express consent for posting
    - `mediaType` 'video' | 'photo' — Optional override. Defaults based on provided media items.
    - `videoCoverTimestampMs` integer — Optional for video posts. Timestamp in milliseconds to select which frame to use as thumbnail (defaults to 1000ms/1 second). Ignored when videoCoverImageUrl is provided.
    - `videoCoverImageUrl` string, uri — Optional for video posts. URL of a custom thumbnail image (JPG, PNG, or WebP, max 20MB). Any downloadable URL works: we rehost it ourselves. The image is stitched as a single frame at the start of the video to serve as the cover. Accounts connected through the TikTok for Business app hand it to TikTok as the cover instead, with no stitching, falling back to videoCoverTimestampMs without it. Overrides videoCoverTimestampMs when provided.
    - `photoCoverIndex` integer — Optional for photo carousels. Index of image to use as cover, 0-based (defaults to 0/first image).
    - `autoAddMusic` boolean — When true, TikTok may add recommended music (photos only). With the brand-organic or branded-content toggle on, TikTok allows Commercial Music Library tracks only, so this attaches nothing there; use musicSoundInfo instead.
    - `musicSoundInfo` object — Commercial Music Library track to attach. Accounts connected through the TikTok for Business app only: a developer-app account rejects the post at publish time with a message that says so. Pick musicSoundId from GET /v1/accounts/{accountId}/tiktok/commercial-music. Ignored on drafts, where TikTok ignores every post_info field.
      - `musicSoundId` string, required — The id field of a track from GET /v1/accounts/{accountId}/tiktok/commercial-music (a song clip id). TikTok fails the publish with a generic 51065 when given the commercial music id instead.
      - `musicSoundVolume` integer — Track volume. TikTok defaults an omitted volume to 0, which publishes the track silently, so we default to the app's 50. Video posts only.
      - `musicSoundStart` integer — Start point of the track in milliseconds (default 0). Video posts only.
      - `musicSoundEnd` integer — End point of the track in milliseconds (default: the video length). Must be greater than musicSoundStart. Video posts only.
    - `videoOriginalSoundVolume` integer — Volume of the video's own sound when a commercial track is attached (0 to 100). Requires musicSoundInfo. Video posts only.
    - `videoMadeWithAi` boolean — Set true to disclose AI-generated content. Accounts connected through the TikTok for Business app carry the disclosure on video posts only: the business photo endpoint has no AI disclosure field, so true on a direct photo post is rejected at creation rather than published undisclosed. Send draft true to publish such a photo post and set the disclosure in the TikTok app.
    - `locationId` string — Location tag to attach, as the id of a result from GET /v1/accounts/{accountId}/tiktok/locations. Accounts connected through the TikTok for Business app and video posts only: a developer-app account rejects the post at publish time with a message that says so, and a direct photo post is rejected at creation since the business photo endpoint has no location field. Requires locationName. Ignored on drafts, where TikTok ignores every post_info field.
    - `locationName` string — Display name of the location tag, as returned next to its id. Required with locationId; a locationId without it is rejected at creation.
    - `isAdsOnly` boolean — Set true to publish the video as an "Only show in ads" post: it is kept off the profile and exists to be used as a Spark Ad. Accounts connected through the TikTok for Business app and video posts only, with the same rejections as locationId. Ignored on drafts.
    - `description` string — Optional long-form caption for photo posts (max 4000 chars). Recommended when content exceeds 90 chars, as photo titles are auto-truncated. Falls back to the post content when omitted.
  - `facebookSettings` FacebookSettings — Facebook options that must be nested under platformSpecificData.facebookSettings, or sent at the request root as facebookSettings. The remaining Facebook options sit directly on platformSpecificData, see FacebookPlatformData.
    - `draft` boolean — When true, creates the post as a draft in Facebook Publishing Tools instead of publishing immediately. Supported for feed posts (text, link, image, video) and reels. Not supported for stories. Drafts expire after ~30 days.
    - `carouselCards` object[] — Renders the post as a multi-link carousel (organic Page post). When set, mediaItems must be provided with the same length and all items must be images (no videos). Each cards[i] adds the click-through link and headline for the image at mediaItems[i]. Mutually exclusive with contentType=story|reel. Facebook display truncates name at ~35 chars and description at ~30 chars; longer strings are accepted but get truncated on render.
      - `link` string, uri, required — Per-card click destination (required).
      - `name` string — Per-card headline (optional, ~35-char display).
      - `description` string — Per-card subhead (optional, ~30-char display).
    - `carouselLink` string, uri — Optional top-level "See more" destination shown on the carousel end card. Defaults to the first card's link when omitted. Only used together with carouselCards.
    - `textFormatPresetId` string — Facebook-defined preset ID that renders the post as large text on a colored background (Graph `text_format_preset_id`). Supply the raw numeric ID from Meta; we do not publish a catalog of presets and Facebook may change the available set. Pages only (ignored on personal profiles and groups) and text-only feed posts only: the request is rejected with 400 when mediaItems or carouselCards are present, when contentType is story or reel, or when content is empty. An attachment makes Facebook drop the background silently, so those are rejected up front. Length is NOT rejected: Facebook's composer stops offering a background at around 130 characters, but Meta documents no API limit, so longer content publishes and returns a warning instead. A URL detected in the content is NOT attached as a link preview while a preset is set, because a link attachment also makes Facebook drop the background.
  - `recycling` RecyclingConfig — Configure automatic post recycling (reposting at regular intervals). After the post is published, the system creates new scheduled copies at the specified interval until expiration conditions are met. Supports weekly or monthly intervals. Maximum 10 active recycling posts per account. YouTube and TikTok platforms are excluded from recycling. Content variations are recommended for X and Pinterest to avoid duplicate flags.
    - `enabled` boolean — Set to false to disable recycling on this post
    - `gap` integer — Number of interval units between each repost. Required when enabling recycling.
    - `gapFreq` 'week' | 'month' — Interval unit for the gap. Defaults to 'month'.
    - `startDate` string, date-time — When to start the recycling cycle. Defaults to the post's scheduledFor date.
    - `expireCount` integer, nullable — Stop recycling after this many copies have been created. Send null on update to clear this limit.
    - `expireDate` string, date-time, nullable — Stop recycling after this date, regardless of count. Send null on update to clear this limit.
    - `contentVariations` string[] — Array of content variations for recycled copies. On each recycle, the next variation is used in round-robin order. Recommended for X and Pinterest to avoid duplicate content flags. If omitted, the original post content is used for all recycled copies. Send an empty array [] to clear existing variations. Must have 2+ entries when setting variations. Platform-level customContent still overrides the base content per platform.

## Response `200`

Post updated

- PostUpdateResponse
  - `message` string
  - `post` Post
    - `_id` string
    - `userId` union
      - string
      - User
        - `_id` string
        - `email` string
        - `name` string
        - `role` string
        - `createdAt` string, date-time
    - `title` string — Stored on the post for reference/display only. This field is NOT used as the video title when publishing. To set a YouTube video title, use platformSpecificData.title on the youtube platform target (falls back to the first line of content when omitted).
    - `content` string
    - `mediaItems` MediaItem[]
      - `type` 'image' | 'video' | 'gif' | 'document'
      - `url` string, uri — A media item with a null, missing or empty url is dropped.
      - `title` string — Optional title for the media item. Used as the document title for LinkedIn PDF/carousel posts. If omitted, falls back to the post title, then the filename.
      - `altText` string — Accessibility alternative text for an image, applied on every platform that supports it: Instagram (feed images only, not Reels/Stories), Facebook, Threads, X (max 1000 chars), LinkedIn, Bluesky, and Pinterest (max 500 chars). Ignored on platforms without alt-text support (TikTok, YouTube, Snapchat, Telegram, Reddit, Google Business Profile, WhatsApp) and on video items where the platform does not accept it. Set once per image; the same value is sent to each selected platform.
      - `filename` string
      - `size` integer — Optional file size in bytes
      - `mimeType` string — Optional MIME type (e.g. image/jpeg, video/mp4)
      - `thumbnail` string, uri — Optional custom thumbnail/cover image URL for videos. Supported for Facebook video posts, Facebook Reels, regular video uploads, and LinkedIn video posts. Max 10MB, JPG/PNG recommended.
      - `instagramThumbnail` string, uri — Custom cover image URL for Instagram Reels. Can also be set via platformSpecificData.instagramThumbnail or platformSpecificData.reelCover. Resolution order: this field > platformSpecificData.instagramThumbnail > platformSpecificData.reelCover > platformSpecificData.thumbnailUrl (legacy).
      - `tiktokProcessed` boolean — Internal flag indicating the image was resized for TikTok
    - `platforms` PlatformTarget[]
      - `platform` string — Supported values: twitter, threads, instagram, youtube, facebook, linkedin, pinterest, reddit, tiktok, bluesky, googlebusiness, telegram
      - `accountId` union
        - string
        - SocialAccount
          - `_id` string, required
          - `platform` 'tiktok' | 'instagram' | 'facebook' | 'youtube' | 'linkedin' | 'twitter' | 'threads' | 'pinterest' | 'reddit' | 'bluesky' | 'googlebusiness' | 'telegram' | 'snapchat' | 'discord' | 'slack' | 'whatsapp' | 'shopify' | 'wordpress' | 'linkedinads' | 'metaads' | 'pinterestads' | 'tiktokads' | 'xads' | 'googleads' | 'openaiads' | 'sms' | 'phone' | 'rcs', required
          - `profileId` union, required
            - string
            - Profile
              - …
          - `username` string
          - `displayName` string
          - `profilePicture` string, nullable — URL to the account's profile picture on the platform. May be null if the platform does not provide one.
          - `profileUrl` string — Full profile URL for the connected account on its platform.
          - `isActive` boolean, required
          - `needsReconnection` boolean — The platform definitively reported the stored OAuth token as dead. While true, GET /v1/connect/{platform}/ads returns a fresh authUrl (implicit force=true) instead of alreadyConnected, so re-running the connect flow recovers the account. Cleared automatically when the account is re-authorized.
          - `followersCount` number — Follower count (only included if user has analytics add-on)
          - `followersLastUpdated` string, date-time — Last time follower count was updated (only included if user has analytics add-on)
          - `parentAccountId` string, nullable — Reference to the parent posting SocialAccount. Set for ads accounts that share or derive from a posting account's OAuth token. null for standalone ads (Google Ads) and all posting accounts. Meta ads business-login accounts also have no parent.
          - `enabled` boolean — Whether the user explicitly activated this account. false means the account was created as a side effect (e.g., posting account auto-created when user connected ads first). Such accounts are hidden from this list, cannot be posted to (`ACCOUNT_NOT_ENABLED_FOR_POSTING`), and are not billed as connected accounts.
          - `metadata` object — Platform-specific metadata. Fields vary by platform. For WhatsApp accounts, includes: - qualityRating: Phone number quality rating from Meta (GREEN, YELLOW, RED, or UNKNOWN) - nameStatus: Display name review status (APPROVED, PENDING_REVIEW, DECLINED, or NONE). A declined or pending display name does not by itself block sending; sendability is reported separately via health_status (can_send_message). - messagingLimitTier: Maximum unique business-initiated conversations per 24h rolling window (TIER_250, TIER_1K, TIER_10K, TIER_100K, or TIER_UNLIMITED). Scales automatically as quality rating improves. - verifiedName: Meta-verified business display name - displayPhoneNumber: Formatted phone number (e.g., "+1 555-123-4567") - wabaId: WhatsApp Business Account ID - phoneNumberId: Meta phone number ID For Meta ads business-login accounts: - tokenType: system-user - businessId: The owning Business Manager ID when there is one owner; null for multiple owners. - businessIds: Owning Business Manager IDs discovered from granted ad accounts. - grantedAdAccountIds: Ad-account IDs granted to the token. - adAccountBusinesses: Map from ad-account ID to its owning business ID or null. - availablePages: Granted Page IDs and names. No Page tokens are exposed. - selectedPageId: The Page selected for creatives and lead forms, or null. - scopedAdAccountIds: Existing sync scope preserved on reconnect. Non-expiring tokens have no tokenExpiresAt field. Parent posting reconnects do not replace this token. For LinkedIn accounts, profileData carries the profile details refreshed on each daily snapshot: - profileData.bio: The member's headline for personal accounts, or the organization description for organization accounts. null when the member has not set one. - profileData.extraData.vanityName: The member's profile slug, i.e. the /in/{vanityName} segment of profileUrl. Personal accounts only; an organization's own slug is in metadata.organizationInfo.vanityName. For X (Twitter) accounts: - profileData.extraData.isPremium: Whether X reports a paid subscription (Basic, Premium, Premium+, or a blue verified badge), which raises the post length limit from 280 to 25,000 characters. Read live at connect and reconnect and refreshed by the daily follower snapshot; because X intermittently reports no subscription for subscribed accounts, a cancellation is stored on the fourth consecutive daily snapshot that reports it (about four days). Accounts connected before the extraData layout carry the same flag at profileData.isPremium.
      - `customContent` string — Platform-specific text override. When set, this content is used instead of the top-level post content for this platform. Useful for tailoring captions per platform (e.g. keeping tweets under 280 characters).
      - `customMedia` MediaItem[]
        - `type` 'image' | 'video' | 'gif' | 'document'
        - `url` string, uri — A media item with a null, missing or empty url is dropped.
        - `title` string — Optional title for the media item. Used as the document title for LinkedIn PDF/carousel posts. If omitted, falls back to the post title, then the filename.
        - `altText` string — Accessibility alternative text for an image, applied on every platform that supports it: Instagram (feed images only, not Reels/Stories), Facebook, Threads, X (max 1000 chars), LinkedIn, Bluesky, and Pinterest (max 500 chars). Ignored on platforms without alt-text support (TikTok, YouTube, Snapchat, Telegram, Reddit, Google Business Profile, WhatsApp) and on video items where the platform does not accept it. Set once per image; the same value is sent to each selected platform.
        - `filename` string
        - `size` integer — Optional file size in bytes
        - `mimeType` string — Optional MIME type (e.g. image/jpeg, video/mp4)
        - `thumbnail` string, uri — Optional custom thumbnail/cover image URL for videos. Supported for Facebook video posts, Facebook Reels, regular video uploads, and LinkedIn video posts. Max 10MB, JPG/PNG recommended.
        - `instagramThumbnail` string, uri — Custom cover image URL for Instagram Reels. Can also be set via platformSpecificData.instagramThumbnail or platformSpecificData.reelCover. Resolution order: this field > platformSpecificData.instagramThumbnail > platformSpecificData.reelCover > platformSpecificData.thumbnailUrl (legacy).
        - `tiktokProcessed` boolean — Internal flag indicating the image was resized for TikTok
      - `scheduledFor` string, date-time — Optional per-platform scheduled time override (uses post.scheduledFor when omitted)
      - `platformSpecificData` object — The platform-specific options stored on this target, echoed back as they were sent. Typed per platform on the way in (see the *PlatformData schemas on the request body); free-form on the way out, because a response is not guaranteed to match exactly one of those variants and generated clients that pick a variant by structure reject the entire response when it doesn't. Zernio's internal publishing state (snapshots, container ids, publish stage) is never returned here, and the key is omitted rather than sent as an empty object.
      - `status` string — Platform-specific status: pending, processing, uploading, published, failed, cancelled (removed from the platform via DELETE /v1/posts/{postId}/unpublish)
      - `platformPostId` string — The native post ID on the platform (populated after successful publish)
      - `platformPostUrl` string, nullable — Public URL of the published post. Included in the response for immediate posts; for scheduled posts, fetch via GET /v1/posts/{postId} after publish time. Empty when the platform confirmed the publish without returning an id a permalink can be built from (TikTok returns a publish id for some uploads); the TikTok reconcile cron backfills it later. If TikTok never returns one, /v1/analytics reports that entry as syncStatus unavailable.
      - `publishedAt` string, date-time — Timestamp when the post was published to this platform
      - `removedFromPlatformAt` string, date-time, nullable — Set when a post that was successfully published later disappears from the platform (deleted on-platform or taken down by the platform). status stays "published" (it reflects the publish outcome); poll this field to detect post-publish removals. Absent while the post is live, and cleared if the post reappears. Detection runs with the analytics sync, so expect up to a few hours of lag.
      - `isTrialReel` boolean — Present and true only when this Instagram reel was launched as a Trial through Zernio (created with platformSpecificData.trialParams). Use it to segment trial reels in analytics. Note: Instagram's Graph API exposes no readable trial field, so this reflects creation-time intent only. It indicates the reel STARTED as a trial, not whether or when it graduated.
      - `trialGraduationStrategy` 'MANUAL' | 'SS_PERFORMANCE' — Graduation strategy the trial reel was launched with. Present only when isTrialReel is true.
      - `errorMessage` string — Human-readable error message when status is failed. Contains platform-specific error details explaining why the publish failed.
      - `errorCategory` 'auth_expired' | 'user_content' | 'user_abuse' | 'account_issue' | 'platform_rejected' | 'platform_error' | 'platform_rate_limit' | 'quota_exhausted' | 'system_error' | 'unknown' — Error category for programmatic handling: auth_expired (token expired/revoked), user_content (wrong format/too long), user_abuse (rate limits/spam), account_issue (config problems), platform_rejected (policy violation), platform_error (5xx/maintenance), platform_rate_limit (platform throttling, retried automatically), quota_exhausted (a shared quota pool the integration draws on is empty, including our own capacity gate in front of one; not caused by your content or account, and safe to retry once the pool frees up), system_error (Zernio infra), unknown
      - `errorSource` 'user' | 'platform' | 'system' — Who caused the error: user (fix content/reconnect), platform (outage/API change), system (Zernio issue, rare)
    - `scheduledFor` string, date-time
    - `timezone` string
    - `status` 'draft' | 'scheduled' | 'publishing' | 'published' | 'partial' | 'failed' | 'cancelled' — `cancelled` is set by DELETE /v1/posts/{postId}/unpublish once every platform entry has been removed from its platform (a post with published entries left becomes `partial`); cancelled posts can be edited and rescheduled like drafts.
    - `tags` string[] — YouTube constraints: each tag max 100 chars, combined max 500 chars, duplicates removed.
    - `hashtags` string[] — Stored for reference only. Hashtags are NOT automatically appended to the caption when publishing. Include hashtags directly in the content field (platforms like Instagram only support hashtags as caption text). For YouTube keywords, use the tags field instead.
    - `mentions` string[] — Stored for reference only. This field does NOT automatically create @mentions when publishing. For LinkedIn @mentions, use the /v1/accounts/{accountId}/linkedin-mentions endpoint to resolve profile URLs to URNs, then embed the returned mentionFormat directly in the post content field.
    - `visibility` 'public' | 'private' | 'unlisted'
    - `metadata` object
    - `recycling` RecyclingState — Current recycling configuration and state on a post
      - `enabled` boolean — Whether recycling is currently active
      - `gap` integer — Number of interval units between reposts
      - `gapFreq` 'week' | 'month' — Interval unit (week or month)
      - `startDate` string, date-time
      - `expireCount` integer
      - `expireDate` string, date-time
      - `contentVariations` string[] — Content variations for recycled copies (if configured)
      - `contentVariationIndex` integer — Current position in the content variations rotation (read-only)
      - `recycleCount` integer — How many recycled copies have been created so far (read-only)
      - `nextRecycleAt` string, date-time — When the next recycled copy will be created (read-only)
      - `lastRecycledAt` string, date-time — When the last recycled copy was created (read-only)
    - `recycledFromPostId` string — ID of the original post if this post was created via recycling
    - `queuedFromProfile` string — Profile ID if the post was scheduled via the queue
    - `queueId` string — Queue ID if the post was scheduled via a specific queue
    - `createdAt` string, date-time
    - `updatedAt` string, date-time
  - `warnings` string[]

## Other responses

- `207` — The post was updated, but the inline publish that followed did not fully succeed. **207 is a 2xx status**, so `fetch(...).ok` is `true` and axios resolves. Branch on the status code explicitly. Read `post.status`: `partial` (some platforms published), `failed` (none published, terminal), or `scheduled` (transient errors, platforms reset to `pending`, Zernio retries automatically and this is not a failure). `platformResults` is omitted when the attempt aborted before producing per-platform results; `post.platforms[]` is always present.
- `400` — Invalid request
- `401` — Unauthorized
- `403` — Forbidden
- `404` — Resource not found
- `409` — The requested scheduledFor collides with another post already occupying that slot in the same queue (code: queue_slot_conflict). Choose a different time, omit scheduledFor and let the queue assign the next open slot, or send queueId: null to schedule this post outside the queue.

## Changes

- **2026-09-17** `fdaf9a9fb1a7` — 4 warning
  - added the new `shopify` enum value to the `post/platforms/items/accountId/oneOf[#/components/schemas/SocialAccount]/platform` response property for the response status `200`
  - added the new `shopify` enum value to the `post/platforms/items/accountId/oneOf[#/components/schemas/SocialAccount]/platform` response property for the response status `207`
  - added the new `wordpress` enum value to the `post/platforms/items/accountId/oneOf[#/components/schemas/SocialAccount]/platform` response property for the response status `200`
  - added the new `wordpress` enum value to the `post/platforms/items/accountId/oneOf[#/components/schemas/SocialAccount]/platform` response property for the response status `207`
- **2026-09-16** `3e6ddf2a99ea` — 3 info
  - added the optional property `details/budgetScope` to the response with the `400` status
  - added the optional property `details/budgetScope` to the response with the `403` status
  - added the optional property `details/budgetScope` to the response with the `409` status
- **2026-09-15** `0dba7d004d75` — 9 info
  - added the new optional request property `tiktokSettings/isAdsOnly`
  - added the new optional request property `tiktokSettings/locationId`
  - added the new optional request property `tiktokSettings/locationName`
  - added the optional property `details/quotaExhausted` to the response with the `400` status
  - …5 more
- …earlier changes not shown

[Full history](https://skmtc.dev/zernio/apis/zernio-api/changes/v1/posts/:postId/put.md)

---

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