---
title: "Update post"
method: PATCH
path: "/api/businesses/{business_id}/posts/{post_id}"
tags: ["posts"]
---

# Update post

`PATCH /api/businesses/{business_id}/posts/{post_id}`

## Request

- Base URL: `https://api.mymarky.ai`
- URL: `https://api.mymarky.ai/api/businesses/{business_id}/posts/{post_id}`
- Auth: HTTP bearer

Update a post's caption, review status, target platforms, media, or platform overrides.

`status` moves the post through YOUR TEAM's review workflow: set NEW to tell
your team the post is ready for their review (it appears on the app's review
page), DRAFT to record a reviewer's approval, REJECTED to record a decline.
Marky does not gate publishing on these — they are your team's workflow
state. Status can only change on posts currently in NEW, DRAFT, or REJECTED:
scheduling has its own endpoint, and published posts are immutable.

`platform_overrides` replaces the post's whole override set: send the full
list to keep, `[]` to clear, or omit the field to leave overrides untouched.

Errors:
    400 invalid_request: The post is already published, no fields were given to update, or status was sent for a post that is SCHEDULED, PUBLISHED, GENERATING, or FAILED.
    401 authentication_required: API key or token is missing, invalid, or revoked.
    403 permission_denied: Your plan does not include API access, or this business is not in your org.
    404 resource_not_found: No post with that ID exists, or it is not in this business.
    409 MEDIA_RECENTLY_USED: (Marky's assistant only) media_ids adds a library photo that is already on another recent post. API-key callers get a `media_recently_used` warning instead.
    429 rate_limit_exceeded: You sent too many requests this minute.

## Path parameters

- `business_id` string, required: A UUID identifier.
- `post_id` string, required: A UUID identifier.

## Headers

- `Marky-Version` '2026-04-08' | '2026-07-17' | '2026-08-02' | '2026-08-04' | '2026-08-06'

## Request body

- PostUpdateRequest
  - `caption` string, nullable: Updated caption text. Note that a channel listed in platform_overrides with its own caption publishes THAT caption, not this one. To change what every channel publishes, either send platform_overrides in the same call with the new per-channel captions, or send platform_overrides: [] to drop the overrides so every channel falls back to this caption. Read the post first to see which channels have one.
  - `status` string, nullable: Move this post through your team's review workflow. NEW = notify your team the post is ready for review (it shows up on the app's review page). DRAFT = a reviewer approved it. REJECTED = a reviewer declined it. These are your team's workflow state — Marky does not gate publishing on them. Only allowed on posts currently in NEW, DRAFT, or REJECTED (use the schedule endpoint to schedule; published posts are immutable).
  - `restrict_publish_to` string[], nullable: Restrict publishing to these platforms (e.g. instagram, facebook, linkedIn). Case-insensitive. Omit or set null to publish to ALL connected platforms.
  - `media_ids` string[], nullable: Replace the attached media, by library id. Marky media ids to attach, in order. Prefer this over media_urls whenever the media is already in Marky — an image you just generated (use its `media_id`), an upload, or a library search result. Ids are verified against your workspace, so a mistyped one fails clearly instead of looking like a broken link. Cannot be combined with media_urls or design_id. SEVERAL ids on ONE post IS a carousel: they publish as a single multi-image post in the order you list them (Instagram assembles them into a carousel, LinkedIn into a swipeable document). There is no separate carousel endpoint and you do not need one — never split the slides across several posts.
  - `allow_media_reuse` boolean: Leave false. Set true only when the user asked to post a specific photo again (a throwback, a signature shot). Without it, a library photo another recent post already uses is refused with 409 MEDIA_RECENTLY_USED; search the library again and pick a photo you have not used.
  - `media_urls` string[], nullable: Replace the attached media. URLs to images or videos. Use this to swap the image on a generated post during review. Prefer media_ids when the media is already in Marky. Do not set this when design_id is given — media comes from the design automatically.
  - `design_id` string, nullable: Attach a design (from create_design) to this post. When set, media is taken from the design automatically — do NOT also pass media_urls. Editing the design later keeps this post's media in sync.
  - `cover_media_id` string, nullable: Replace this video's cover image. Marky media id of an IMAGE to use as this video's cover (the thumbnail shown before it plays). Upload the still with `POST /api/media/from-url` or `POST /api/media/uploads` first and pass the id it returns. Used by Instagram (Reel cover), Facebook and Pinterest. TikTok can only take a frame of the video itself, so it uses `video_cover_timestamp_ms` instead; LinkedIn, X, YouTube and Google Business pick their own. Ignored on a post with no video. Replacing the post's media later does NOT clear the cover — send a new one.
  - `video_cover_timestamp_ms` integer, nullable: Which frame of the video TikTok should use as its cover, in milliseconds from the start (TikTok's API takes a timestamp, never an image). Defaults to 1000 (one second in). Other platforms ignore this and use `cover_media_id` when you send one.
  - `link` string, nullable: Clickable destination URL for platforms that support link posts: the Facebook link attachment, the Google Business CTA button, and the Pinterest pin destination. Other platforms ignore it — put links in the caption there. Must be http(s).
  - `metadata` object, nullable: Up to 50 string key/value pairs for your own analytics dimensions (e.g. {'media_type': 'animated-video', 'format': 'talking-head', 'style': 'data-take', 'hook': 'question', 'topic': 'automate-with-ai'}). Keys <=40 chars, values <=500 chars. Returned verbatim on every read; Marky never interprets them.
  - `platform_overrides` PlatformOverrideModel[], nullable: REPLACES the post's entire override set: send the full list you want to keep, send [] to clear all overrides, or omit/null to leave them unchanged.
    - `platform` string, required: Platform this override applies to (e.g. instagram, linkedIn, twitter). Case-insensitive.
    - `caption` string, nullable: Caption this platform gets instead of the post's caption. Tailor length, hashtag count, and link placement to the platform.
    - `media_urls` string[], nullable: Media this platform gets instead of the post's media_urls.
    - `title` string, nullable: Title, on platforms that have one (Pinterest pin title, YouTube video title).
    - `link` string, nullable: Destination link, on platforms that support one (e.g. Pinterest, Google Business).
    - `first_comment` string, nullable: Comment auto-posted right after publishing on this platform — the usual home for hashtags and links on Instagram/LinkedIn.
  - `restrict_publish_to_integration_ids` string[], nullable: Restrict publishing to these exact connected accounts (integration ids from `GET /businesses/{business_id}/integrations`). Use instead of restrict_publish_to when a business has multiple accounts of the same platform and you want specific ones. Omit or set null for platform-level targeting. Cannot be combined with restrict_publish_to in one request.

## Response `200`

Successful Response

- PostResponse
  - `id` string, required: Post ID
  - `business_id` string, required: Business this post belongs to
  - `caption` string, nullable: Post caption text
  - `status` string, nullable: Post status. One of: NEW (draft awaiting your team's review, incl. freshly generated), DRAFT (approved by a reviewer on your team), REJECTED (declined by a reviewer), SCHEDULED (queued for a future time), PUBLISHED (already posted). DRAFT/REJECTED are your team's review verdicts — Marky does not gate publishing on them. Same values the `list_posts` status filter accepts.
  - `media_urls` string[], nullable: Attached media URLs
  - `media_details` PostMedia[], nullable: The same attached media as `media_urls`, in the same order, but with each item's description and transcript. Use this when analysing what content performs: the creative is usually the reason a post did well, and a URL alone tells you nothing about what was in it.
    - `id` string, required: Media ID in the business's library
    - `url` string, nullable: Public URL of the media
    - `type` string, nullable: 'image', 'video', or 'audio'
    - `description` string, nullable: What the media shows, generated by a vision model — including any headline or offer text rendered into the graphic, which on a designed post is usually the whole message. Null if not described yet.
    - `transcript` string, nullable: Spoken words in the video or audio, if any.
    - `duration` number, nullable: Length in seconds, for video and audio.
  - `design_id` string, nullable: The design attached to this post, if any. Media is derived from this design; edit it with revise_post_design or create_design + a new link.
  - `cover_media_id` string, nullable: Media id of the cover image shown before this post's video plays. Marky generates one from the first frame when you don't send one.
  - `video_cover_timestamp_ms` integer, nullable: The frame (milliseconds from the start) TikTok uses as its cover. Null when nobody has chosen a cover for this post.
  - `link` string, nullable: Clickable destination URL for platforms that support link posts: the Facebook link attachment, the Google Business CTA button, and the Pinterest pin destination. Other platforms ignore it — put links in the caption there. Must be http(s).
  - `restrict_publish_to` string[], nullable: Platforms this post is restricted to. Null means ALL connected platforms.
  - `restrict_publish_to_integration_ids` string[], nullable: Exact connected accounts (integration ids) this post is restricted to. Null means targeting is platform-level (see restrict_publish_to).
  - `scheduled_publish_time` string, nullable: The exact time this post is scheduled to publish (set via `schedule`). Null for drafts, queued posts, and already-published posts.
  - `scheduled_publish_time_local` string, nullable: The same moment in the business's timezone, ready to show a person, e.g. "Wed Sep 23, 6:00 PM MSK". Quote it as-is instead of converting the UTC value.
  - `timezone` string, nullable: IANA timezone the `*_local` times are in: the business's calendar timezone, or "UTC" when the business hasn't set one (see `timezone_source`).
  - `timezone_source` 'business' | 'unset_utc', nullable: Where `timezone` came from: `business` (the business's own setting) or `unset_utc` (no timezone saved yet, so times are shown in UTC).
  - `published_at` string, nullable: When the post was actually published (past timestamp). Null until it publishes.
  - `metadata` object, nullable: Caller-defined string key/value pairs set at create/update time — your own analytics dimensions (e.g. media_type, format, style, hook, topic). Marky returns them verbatim and never interprets them. Keys starting with `marky_` are Marky's own (e.g. the copy date below); any you send are dropped.
  - `copy_date` string, date, nullable: The day this caption is about, read from the caption when it was written: an upcoming holiday of the business's country, one of the business's events, or an explicit date like 'September 25'. Never 'today'/'tomorrow'. Queueing never places the post after this day. Null when the caption names no date.
  - `copy_moment` string, nullable: The moment the caption names, e.g. 'National Day'. Null for a plain date.
  - `suggested_publish_time` string, nullable: When to publish on `copy_date`: the business's first posting-schedule slot that day, else 10:00 local. Offer it as the post's date; schedule with `at` to use it.
  - `suggested_publish_time_local` string, nullable: The same moment in the business's timezone, ready to show a person, e.g. "Wed Sep 23, 6:00 PM MSK". Quote it as-is instead of converting the UTC value.
  - `platform_overrides` PlatformOverrideModel[], nullable: Per-platform content overrides on this post, if any. Each platform publishes its override fields where set, the post's own fields otherwise.
    - `platform` string, required: Platform this override applies to (e.g. instagram, linkedIn, twitter). Case-insensitive.
    - `caption` string, nullable: Caption this platform gets instead of the post's caption. Tailor length, hashtag count, and link placement to the platform.
    - `media_urls` string[], nullable: Media this platform gets instead of the post's media_urls.
    - `title` string, nullable: Title, on platforms that have one (Pinterest pin title, YouTube video title).
    - `link` string, nullable: Destination link, on platforms that support one (e.g. Pinterest, Google Business).
    - `first_comment` string, nullable: Comment auto-posted right after publishing on this platform — the usual home for hashtags and links on Instagram/LinkedIn.
  - `publish_results` PublishResultResponse[]: Per-platform publish outcomes — one entry per platform this post was sent to. Poll this until every entry has a terminal status (`success` or `failed`) to know publishing is complete.
    - `platform` string, required: Platform identifier (facebook, instagram, etc.)
    - `status` string, required: Publish status on this platform: success, failed, in_progress, etc.
    - `publish_id` string, nullable: External post ID on the platform — use with `GET /businesses/{business_id}/integrations/{integration_id}/posts/{external_post_id}/stats`
    - `post_url` string, nullable: Public URL of the published post on the platform
    - `error_type` string, nullable: Error type if status is failed (e.g. REFRESH_TOKEN_EXPIRED, TIMEOUT)
    - `error_message` string, nullable: Human-readable error message if status is failed
    - `integration_id` string, nullable: The exact connected account this result belongs to. Distinguishes accounts when a business has several on one platform.
    - `updated_at` string, nullable: When this result was last updated
  - `queue` QueueInfo: Where a post sits in its business's recurring daily publishing queue. Only present on posts that are in a queue (status SCHEDULED with no `adhoc_publish_time`). Lets an API user confirm a queued post is really queued and see roughly when it will publish.
    - `queue_id` string, required: ID of the daily queue this post belongs to.
    - `position` integer, required: 1-based rank among the queued posts (1 = next to publish).
    - `estimated_publish_time` string, nullable: Best estimate of when this post will publish, projected from the queue's recurring timeslots. Ignores jitter and skipped slots, so treat it as approximate. Null if it can't be computed.
    - `estimated_publish_time_local` string, nullable: The same moment in the business's timezone, ready to show a person, e.g. "Wed Sep 23, 6:00 PM MSK". Quote it as-is instead of converting the UTC value.
    - `queue_runs_dry_at` string, nullable: Estimated publish time of the LAST post in this queue: when it runs dry. Same estimate rules as estimated_publish_time.
    - `queue_runs_dry_at_local` string, nullable: The same moment in the business's timezone, ready to show a person, e.g. "Wed Sep 23, 6:00 PM MSK". Quote it as-is instead of converting the UTC value.
    - `warnings` FieldWarning[]: Advisory notes about WHEN this post goes out; they never block. DATE_BOUND_COPY: the caption says 'today', a weekday, 'this week'... and the post lands on a different day. FAR_FUTURE: it lands more than 90 days out. Quote them to the user.
      - `field` string, required: The field this warning is about, e.g. 'strategy', 'caption' or 'platform_overrides[0].caption'.
      - `code` string, nullable: Stable machine-readable code, e.g. 'cta_without_contact'. Branch on this, never on the message. Null on the older size-budget warnings.
      - `level` 'approaching_limit' | 'over_limit' | 'advisory', required: 'approaching_limit' = past the soft limit, act soon. 'over_limit' = past the hard limit, act now. 'advisory' = worth fixing before this publishes, but it may be intended.
      - `message` string, required: Plain-language explanation of what is wrong and how to fix it.
  - `publish_readiness` PublishReadiness: Where this post will actually go, worked out when it was queued, scheduled or published.
    - `will_publish_to` string[], required: Platforms that will receive the post. Empty means nothing connected can publish it.
    - `blocked` BlockedPlatform[]: Accounts the post was aimed at that won't receive it, each with the reason and fix.
      - `platform` string, required: Platform identifier (facebook, instagram, etc.)
      - `integration_id` string, required: The account that won't receive the post.
      - `reason` string, required: Stable reason code, e.g. INTEGRATION_DELETED (disconnected or needs a reconnect), PAGE_NOT_FOUND (no Page selected), MEDIA_FORMAT_ERROR (this platform needs a photo or video).
      - `message` string, required: Plain-language explanation and fix.
      - `reconnect_url` string, nullable: Where the user reconnects this account, when reconnecting is the fix. Null otherwise.
    - `not_connected` string[]: Platforms the post was aimed at (via restrict_publish_to) that have no account connected. The post will NOT go to these. Connect the account, then schedule or update the post again.
  - `repeating_cron` string, nullable: If set, this post repeats: a standard 5-field cron string (e.g. '0 9 * * 1' = every Monday 9am UTC). When it publishes, Marky auto-creates the next occurrence at the next cron time. Null = a one-off post.
  - `created_at` string, required: When the post was created
  - `warnings` FieldWarning[]: Non-blocking notes about this post's captions, e.g. a call-to-action with no link or phone ('cta_without_contact'), or a caption in a different language from the business ('language_mismatch'). Empty when there is nothing to fix. Fix them with update_post before the post publishes; they never block anything.
    - `field` string, required: The field this warning is about, e.g. 'strategy', 'caption' or 'platform_overrides[0].caption'.
    - `code` string, nullable: Stable machine-readable code, e.g. 'cta_without_contact'. Branch on this, never on the message. Null on the older size-budget warnings.
    - `level` 'approaching_limit' | 'over_limit' | 'advisory', required: 'approaching_limit' = past the soft limit, act soon. 'over_limit' = past the hard limit, act now. 'advisory' = worth fixing before this publishes, but it may be intended.
    - `message` string, required: Plain-language explanation of what is wrong and how to fix it.
  - `updated_at` string, nullable: Last update time
  - `revision_id` string, nullable: Id of the undo record this write created. Pass it to revert_revisions to put the record back exactly as it was before this call. Set only on the response to a write; null when you read the record.
  - `url` string, required: Link to view this post in the Marky app (openable/shareable — show it to the user). This is where the post lives in Marky; `link` is the outbound URL published with the post.
  - `delivery` 'pending' | 'delivered' | 'partial' | 'failed', nullable, required: What actually reached the platforms, from publish_results: 'delivered' (every platform succeeded), 'partial' (some succeeded, some failed), 'failed' (every platform failed), 'pending' (still in progress). Null when no publish has been attempted. Prefer this over `status` to tell the user whether a post went out.
  - `scheduled_posts_url` string, nullable, required: Link to the workspace's scheduled-posts list in the Marky app — where the user sees this post waiting in the lineup. Present once the post is SCHEDULED; null otherwise.

## Other responses

- `400`: Bad request — the input was malformed or otherwise invalid.
- `401`: Authentication failed — the API key is missing or invalid.
- `403`: Forbidden — the key is valid but may not access this resource.
- `404`: Not found — no resource matches the id in the path.
- `422`: Validation error — one or more fields failed validation.
- `429`: Rate limited — retry after the window resets (see Retry-After).
- `500`: Server error — something failed on our side; retry or quote X-Request-Id.

---

[API](https://skmtc.dev/mymarky/apis/marky-public-api.md) · [All operations](https://skmtc.dev/mymarky/apis/marky-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mymarky/apis/marky-public-api/revisions/25aeacae455c?raw)
