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.

PATCH/api/public/posts/:id
cURLPATCH /api/public/posts/:id
curl -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..."
  }'
200Response
{
  "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

idstringrequired

Resource id from the corresponding list or create endpoint.

Body parameters

titlestring|null

Max 240 chars; null clears it.

contentstring

LinkedIn 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|array

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

articleobject

X 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|null

Alias: 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.

expectedVersionint

The 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 | null

LinkedIn 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).

postobject

Full post object, same shape as GET.

versionobject|null

{ id, versionNumber, createdAt }; null if content wasn't provided or was unchanged (no-op save).

Errors

400validation_error: none of title/content/tweets/article/integrationId provided
404post_not_found
409post_scheduled_locked: post is scheduled; unschedule first
409post_published_immutable: post already published
404linkedin_profile_not_found (LinkedIn post) / integration_not_found (X post): new integrationId not in this workspace
400x_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
400invalid_x_content: the tweets or article are not valid X content (details.issues lists the problems) / x_content_conflict: tweets and article sent together
400channel_mismatch: integrationId is on the other channel. Create a version for that account with the MCP tool add_post_version instead
400integration_required: an X post cannot be unassigned
409version_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.