Posts
Update post draft
Updates a draft's title, content (LinkedIn) or tweets or article (X), saved as a new version, and/or assigned account. Fails on scheduled/published posts. Pass latestVersion from GET as expectedVersion to avoid overwriting newer edits; a stale version returns 409 version_conflict with the current content.
/api/public/posts/:idcurl -X PATCH https://postiv.ai/api/public/posts/ID_ID \
-H "Authorization: Bearer pk_postiv_..." \
-H "Content-Type: application/json" \
-d '{
"title": "Launch lessons",
"content": "Three things we learned from launch week..."
}'{
"success": true,
"post": {
"id": "post1...",
"status": "draft",
"content": "Updated text..."
},
"version": {
"id": "v2...",
"versionNumber": 2,
"createdAt": "2026-07-19T09:05:00.000Z"
}
}Authorization
Requires an organization API key with the posts:write scope, sent as Authorization: Bearer or x-api-key.
Path parameters
idstringrequiredResource id from the corresponding list or create endpoint.
Body parameters
titlestring|nullMax 240 chars; null clears it.
contentstringLinkedIn posts only. Max 100000 chars; stored as a new post_versions row. On an X post it returns 400 x_content_shape: pass tweets or article.
tweetsstring|arrayX tweet or thread posts only. A tweet string, or an array (a thread) of tweet strings or { text, assets? }. Replaces all tweets: a tweet that omits assets keeps the images at its position, assets: [] removes them.
articleobjectX article posts only: { headline (max 300), body (max 100000, optional) }. headline is plain text; body is markdown: # ## ### headings, bold, *italic*, text links, - and 1. lists, > quotes, a blank line between paragraphs and a single newline for a line break. Markdown images are refused (400 invalid_x_content): place images with attach-post-media. The cover and inline images are kept, each after the paragraph it followed. Omit body (or send it back unchanged) to keep the body exactly as it is.
integrationIdint|nullAlias: integration_id. null unassigns a LinkedIn post. An X post can only move to another X account and a LinkedIn post to a LinkedIn account (400 channel_mismatch otherwise); an X post cannot be unassigned.
expectedVersionintThe latestVersion you read from GET /api/public/posts/:id. Sent with content, tweets or article: if the post has a newer version the edit is refused with 409 version_conflict (details.latestVersion and details.current hold the latest content) instead of overwriting it. Omit it to save over whatever is there. Title-only and integrationId-only updates ignore it.
linkPreviewUrlstring | nullLinkedIn only: move the preview card to one of the links in the post text (as saved by this update), or null to remove it. Omit to leave it: a content edit keeps the card on its link while that link stays in the text, and a newly added link gets the card when there is none. 400 invalid_link_preview_url when the link is not in the text; 400 link_preview_linkedin_only on an X post.
Response
The updated post plus the new version (if content changed).
postobjectFull post object, same shape as GET.
versionobject|null{ id, versionNumber, createdAt }; null if content wasn't provided or was unchanged (no-op save).
Errors
| 400 | validation_error: none of title/content/tweets/article/integrationId provided |
| 404 | post_not_found |
| 409 | post_scheduled_locked: post is scheduled; unschedule first |
| 409 | post_published_immutable: post already published |
| 404 | linkedin_profile_not_found (LinkedIn post) / integration_not_found (X post): new integrationId not in this workspace |
| 400 | x_content_shape: content sent to an X post, tweets sent to an X article, or article sent to an X tweet or thread / linkedin_content_shape: tweets or article sent to a LinkedIn post |
| 400 | invalid_x_content: the tweets or article are not valid X content (details.issues lists the problems) / x_content_conflict: tweets and article sent together |
| 400 | channel_mismatch: integrationId is on the other channel. Create a version for that account with the MCP tool add_post_version instead |
| 400 | integration_required: an X post cannot be unassigned |
| 409 | version_conflict: expectedVersion is not the latest version, or the post changed while the edit was applied. Nothing was saved; details.latestVersion and details.current (content in the GET shape) hold what is there now, so re-apply the edit on top and retry |
Good to know
- A no-change content save (identical to the current version) is silently skipped: version returns null rather than creating a duplicate version row.
- Read the post with GET /api/public/posts/:id first, so each tweet's current assets are known, and send its latestVersion as expectedVersion. Without expectedVersion the edit is saved on top of whatever the latest version is.