---
title: "Change a post that hasn't gone out"
method: PATCH
path: "/posts/{id}"
---

# Change a post that hasn't gone out

`PATCH /posts/{id}`

## Request

- Base URL: `https://xpost.to/api/v1`
- URL: `https://xpost.to/api/v1/posts/{id}`
- Auth: HTTP bearer

Caption, attachments, time, per-platform options — anything omitted is kept. It takes the composer's own edit path: a new post supersedes the old one, so guardrails, caps and the signature all run again on the words that will actually be published (and the response carries the NEW id). Any post in the project that hasn't gone out — `pending_approval` / `draft` / `scheduled` / `rejected`, whoever wrote it. **An agent's edit of a post a human approved (or rejected) goes back into the queue**, because the approval was for the old words. A post that has already been tried can't be edited: retry the delivery that failed instead.

## Path parameters

- `id` string, uuid, required

## Request body

- object
  - `caption` string
  - `media_ids` string[]
  - `upload_ticket` string: A ticket from POST /media/ticket that files have been posted to: they become the attachments, in order, after anything in `media_ids`.
  - `add_media_ids` string[]: Appended after what the post already holds (or after `media_ids` when both are sent), duplicates dropped — an edit that adds a picture without knowing the current set.
  - `scheduled_at` string, nullable: A new instant (Z or offset); `next_slot` hands the post to the project's queue; null clears the schedule
  - `platform_configurations` object: Per-platform options, keyed by platform name (x, instagram, facebook, threads, pinterest, tiktok, youtube, linkedin, bluesky). Common keys: `caption` (override the main caption for that platform), `placement` ("reels" | "stories" | "timeline" — instagram/facebook; threads supports reels only; reels need a video, stories need media and print no caption). Pinterest: `title`, `board_ids`, `link`. X: `first_comment` (auto-replied under the tweet), `thread` (up to 4 follow-up tweet texts chained under the main tweet), `reply_settings` (X polls are not supported — a `poll` is refused). Threads poll options are at most 25 characters each; a Threads topic tag at most 50, with no `.` or `&`. Threads: `topic_tag` (one word, no #), `reply_control`, `poll_option_a`…`poll_option_d` (at least two; a poll only rides a post with NO media, and this API is the only way to set one — the dashboard composer has no poll fields), `gif_id` (a giphy.com link, same media-free rule), `crosspost_ig_story`. Platform disclosure labels, set at publish and impossible to add afterwards: Instagram `is_paid_partnership` + `branded_content_sponsors` (max 2 usernames) + `is_ai_generated`; TikTok `is_brand_content` (a third party paid) / `is_organic_brand_content` (your own business) / `is_ai_generated`; YouTube `has_paid_product_placement` + `contains_synthetic_media`; Pinterest `is_ai_generated`. Instagram: `first_comment` too (max 2200 characters, not on stories) — but only for accounts connected through bundle.social, since Instagram refuses the comment on the other route; asking for one anywhere else is refused at create time, naming the account. Every text field here is guardrail-checked exactly like the caption — including keys not listed above, since anything that reaches a platform is checked. Machine values (`placement`, `board_id`, `reply_settings`, `privacy_status` and similar enums and ids) are exempt: they are not places a message can hide.

## Response `200`

Edited — note the new `id`

- object
  - `id` string, uuid
  - `status` string
  - `replaced` string, uuid
  - `scheduled_at` string, date-time, nullable: The slot the edited post holds — inherited from the original when you named no time, and null when there is no slot at all (see `publishes`)
  - `publishes` 'at_time' | 'on_approval' | 'now', nullable: What has to happen before the edited post goes out: a chosen time, a person's approval, nothing at all (`now`), or null when it is parked as a draft.
  - `timezone` string: The project's IANA zone, the clock `scheduled_at` is on
  - `approval_url` string

## Other responses

- `400`: Validation error, or a post in a state that can't be edited
- `401`: Missing or invalid API key
- `403`: Missing posts:write scope
- `404`: Post not found in this project
- `422`: Blocked by project guardrails
- `429`: Rate limited

---

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