---
title: "Publish a saved draft"
method: POST
path: "/v1/posts/{id}/publish"
tags: ["Posts"]
---

# Publish a saved draft

`POST /v1/posts/{id}/publish`

## Request

- Base URL: `https://api.postlake.dev`
- URL: `https://api.postlake.dev/v1/posts/{id}/publish`
- Auth: `bearerAuth` (a scheme the document does not define)

Publish or schedule in place with the original ID and immutable sourceDraftId. Content, campaign, assetMetadata and platform options are preserved. Supply expectedSnapshot from GET to fence every stored field. Identical completed approvals return the existing result without reposting; a claimed or uncertain operation returns 409. Approval-required agents cannot approve themselves. Personal drafts remain editable after every destination fails; approved submissions retain a failed outcome for reconciliation.

## Path parameters

- `id` string, required

## Request body

- object
  - `scheduledAt` string, date-time, nullable: Publish at this time instead. Omit to keep whatever time the draft was carrying (or send now if it had none). Send null to clear the draft's time and publish immediately.
  - `timezone` string: IANA timezone (e.g. Europe/London) for a naive scheduledAt.
  - `accounts` string[]: Publish to these accounts instead of the draft's own destinations.
  - `expectedSnapshot` string: Full-state snapshot from GET. Any intervening edit rejects approval with 409.
  - `profile` string: Publish to every account under this profile instead of the draft's own destinations.
  - `platforms` Platform[]: Optional filter applied to the resolved destinations.

## Response `201`

Published or scheduled

- Post
  - `id` string, required
  - `sourceDraftId` string: Immutable original draft ID. New approvals preserve this as id; legacy audited approvals may resolve an older ID to a different canonical id.
  - `snapshot` string: Computed full-state approval fence returned by GET. Includes media, options and metadata.
  - `state` 'draft' | 'awaiting_approval' | 'rejected' | 'queued' | 'scheduled' | 'processing' | 'partial' | 'published' | 'failed', required
  - `createdAt` string, date-time, required
  - `clientId` string: Stable machine principal that created the post. Absent for human-created dashboard posts.
  - `surface` 'dashboard' | 'api' | 'agent': Where the post was created, independent of the principal identity.
  - `approval` object: Present for approval-gated agent work, including the owner's final decision when one has been made.
    - `requestedAt` string, date-time, required
    - `decision` 'approved' | 'rejected'
    - `decidedAt` string, date-time
    - `reason` string
  - `scheduledAt` string, date-time, nullable, required
  - `timezone` string: IANA timezone the caller scheduled in, when they named one.
  - `scheduledAtLocal` string: Wall-clock time in timezone, computed on read. Not stored.
  - `text` string, required
  - `campaign` string
  - `assetMetadata` object
  - `media` string[], required
  - `warnings` string[]: Advisory notes from create or edit, for example PNG converted to JPEG for TikTok.
  - `platformOptions` object, nullable, required
    - `pinterest` object
      - `boardId` string
      - `link` string
      - `altText` string
  - `targets` PostTarget[], required
    - `removal` object: Present after accepted network removal on a partial result. Retry skips accepted destinations; verified separately confirms absence.
      - `accepted` true, required
      - `verified` boolean, required
      - `at` string, date-time, required
    - `account` string, required
    - `platform` 'bluesky' | 'threads' | 'x' | 'linkedin' | 'instagram' | 'tiktok' | 'facebook' | 'youtube' | 'pinterest', required
    - `state` 'queued' | 'scheduled' | 'processing' | 'published' | 'failed', required
    - `profile` string: Username of the profile this channel belongs to. Resolved when the post is read, so it follows the channel if it moves. Absent once the channel is disconnected.
    - `channelName` string: The channel's handle at the time of reading, not the one frozen at publish. Absent once the channel is disconnected.
    - `connection` object: The native account or destination identity captured when this target was scheduled. It is used only to repair a disconnected channel safely.
      - `platformAccountId` string
      - `destinationId` string
      - `destinationName` string
      - `handle` string
    - `remoteId` string, nullable, required
    - `url` string, nullable, required
    - `publishedAt` string, date-time, nullable, required
    - `permalinkPending` boolean: True when publication succeeded but an exact public permalink is still being resolved.
    - `error` NormalisedError, required: `type` is the coarse category you route on and is deliberately small, so it cannot say WHICH input was wrong. `code`, `fix`, `docs` and `param` are what turn a rejection into something a caller can act on rather than guess at. They are optional only in the sense that not every error has all four.
      - `type` string, required: Coarse category to route on, e.g. rate_limited, auth_expired, platform_rejected.
      - `message` string, required
      - `platform` 'bluesky' | 'threads' | 'x' | 'linkedin' | 'instagram' | 'tiktok' | 'facebook' | 'youtube' | 'pinterest'
      - `retryable` boolean, required
      - `code` string: Stable, granular handle to branch on, e.g. text_too_long. Also the anchor in the error reference.
      - `fix` string: The next action, in plain words. Safe to show a person as-is.
      - `docs` string, uri: Where the rule is written down, so it is learned once rather than rediscovered.
      - `param` string: The request field at fault, so a form can mark the right input.

## Other responses

- `400`: That post is not a draft, or the content is invalid
- `401`: Unauthorized
- `402`: Not enough credits. The draft is left untouched.
- `404`: Draft not found
- `409`: The reviewed content changed, another approval claimed the post, or the provider outcome needs reconciliation. Read the existing post; do not create a replacement.
- `429`: Rate limited

---

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