---
title: "Update a post by slug"
method: PUT
path: "/v1/posts/slug/{slug}"
tags: ["posts"]
---

# Update a post by slug

`PUT /v1/posts/slug/{slug}`

Update an existing post using its URL slug. The publication is identified by the API key provided in the Authorization header.

**Behavior:**
- Only provided fields are updated; omitted fields remain unchanged
- When `markdown` is provided, it replaces the full content. Rich blocks (embeds, buttons, callouts) created in the editor will be lost
- Set `status` to `"published"` to publish a draft, `"draft"` to unpublish, or `"archived"` to archive
- When editing an already-live post that should remain live, include `status: "published"` in the update and verify the returned post status before telling the writer it is live
- Set `scheduledAt` (Unix timestamp in milliseconds) to schedule a draft's first-publish for a future time. Must be in the future and at most 30 days out. Only valid for posts that haven't been published or already scheduled. Pass `scheduledAt: null` to cancel a previously scheduled publish (or to reschedule: cancel first, then schedule again with the new time). Set `sendNewsletter: true` alongside `scheduledAt` to email subscribers when the post publishes.
- Set `imageUrl` to update the post's cover/hero image; the URL is fetched, re-hosted, and a placeholder is generated. Pass `clearImage: true` to remove the existing cover.

## Path parameters

- `slug` string, required

## Request body

- object
  - `markdown` string — Post content in Markdown format. Replaces the FULL body. Markdown cannot represent buttons, linked images, or embedded media (videos, tweets, link cards) — replacing a post that has any of those with `markdown` drops them. Use `bodyJson` (round-tripped from get-post) to edit an existing post so nothing is lost. Provide `markdown` OR `bodyJson`, not both.
  - `bodyJson` string — Post content as a Tiptap document, JSON-stringified (e.g. '{"type":"doc","content":[...]}'). This accepts ANY Tiptap node the editor supports — including videos, tweets, link cards, callouts, and buttons — so editing a post by round-tripping the `json` returned by get-post preserves everything markdown would drop. Replaces the FULL body; node-type validity is checked by the renderer and an unusable document is rejected. Provide `markdown` OR `bodyJson`, not both.
  - `title` string — Title of the post
  - `subtitle` string — Optional subtitle or brief summary
  - `slug` string — URL-friendly identifier for the post
  - `postPreview` string — Preview text for the post
  - `categories` union — Category tags for the post. Can also be a comma-separated string.
    - string[]
    - string
  - `authorIds` string[] — User ids credited as the post's authors, in byline order. Replaces the full list, so include every author you want kept — read the post's current `authorIds` first and start from those. Each id must be the publication's owner or an active team member; ids from outside the publication are rejected. There is no endpoint that enumerates members, so an id has to come from a post you have read (`authorIds` on a single post, or `authors[].id` on a list) or from the writer.
  - `status` 'draft' | 'published' | 'archived' — Set to 'published' to publish a draft or keep an already-live post published after edits, 'draft' to unpublish, or 'archived' to archive
  - `scheduledAt` integer, nullable — Unix timestamp (milliseconds) to schedule the post's first publish at a future time. Must be in the future and at most 30 days out. Only valid for draft posts that haven't been published or already scheduled. Cannot be combined with status: 'draft' or 'archived'. Pass null to cancel a previously scheduled publish. The value 0 is treated the same as omitting the field (no scheduling request); note that on an already-scheduled post, omitting `scheduledAt` while changing `status` cancels the schedule.
  - `sendNewsletter` union — Whether to send an email newsletter to subscribers when the post publishes. Only meaningful when publishing (status: 'published') or scheduling (scheduledAt set). Default: false
    - boolean
    - 'true' | 'false' | '1' | '0'
  - `publishedAt` integer — Unix timestamp (milliseconds) to set as the post's publish date. Once set, the date is preserved across re-publishes.
  - `imageUrl` string, uri — URL of an image to set as the post's cover/hero image. The image is fetched, re-hosted on Paragraph's CDN, and a placeholder is generated. Pass clearImage: true instead to remove the existing cover.
  - `clearImage` boolean — When true, removes the post's existing cover/hero image. Ignored if imageUrl is also provided.

## Response `200`

Post updated successfully

- object
  - `success` true, required — Whether the update succeeded

## Other responses

- `400` — Invalid request - check field values
- `401` — Invalid or missing API key
- `404` — Post not found in your publication
- `409` — Conflict - the body change was refused because you weren't working from the post's current content: either you didn't call get-post before replacing the body, or the writer edited the post in the editor since you read it. Call get-post to read the current version, re-apply your change on it, and retry.
- `500` — Internal server error

---

[API](https://skmtc.net/paragraph-xyz/apis/paragraph-api.md) · [All operations](https://skmtc.net/paragraph-xyz/apis/paragraph-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/paragraph-xyz/paragraph-api/versions/7bbf1b62c1a0/schema)
