---
title: "Publish, schedule or draft a post"
method: POST
path: "/v1/posts"
tags: ["Posts"]
---

# Publish, schedule or draft a post

`POST /v1/posts`

## Request

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

Publish or schedule a post. Set `draft: true` to save it instead. A guarded agent's normal publish request is stored in `awaiting_approval` automatically and cannot be approved by that same agent. Neither state costs anything or contacts a network.

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `text` string, required: Caption for feed posts and Reels. May be empty for an Instagram Story-only post; Instagram Stories do not display captions.
  - `campaign` string: Optional lowercase campaign slug, up to 80 characters.
  - `assetMetadata` object: Up to 16 short caller-defined fields, such as hook, track or source filename.
  - `profile` string: A profile name. Resolves to every account it owns. The simple way to address accounts; combine with `platforms` to narrow it. Provide `profile`, `accounts`, or both.
  - `platforms` Platform[]: Optional filter. Keeps only these networks from the resolved set (e.g. profile "my-brand" + platforms ["bluesky"]).
  - `accounts` string[]: Connected account ids (acc_…). An alternative (or addition) to `profile`.
  - `media` string[]
  - `requireAllTargets` boolean: When true, refuse the whole request before contacting a network if any selected destination fails PostLake's deterministic capability preflight. Default false preserves partial fan-out.
  - `scheduledAt` string, date-time: UTC with a trailing Z, or a naive local time together with timezone.
  - `timezone` string: IANA timezone (e.g. Europe/London). Interprets a naive scheduledAt as wall time in that zone.
  - `draft` boolean: Save without sending. Returns `draft`, or `awaiting_approval` when this agent requires approval. Nothing is charged and no network is contacted.
  - `platformOptions` object
    - `pinterest` object
      - `boardId` string
      - `link` string
      - `altText` string
    - `tiktok` object: See GET /v1/platforms/tiktok for the full option set and valid values.
      - `mode` 'direct' | 'inbox': direct = publish now; inbox = send to the creator's TikTok inbox as a draft (finished inside the TikTok app).
      - `privacyLevel` 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY'
      - `title` string: Photo-post title (video posts use `text` as the caption).
      - `allowComment` boolean
      - `allowDuet` boolean
      - `allowStitch` boolean
      - `coverTimestampMs` integer
      - `photoCoverIndex` integer
      - `autoAddMusic` boolean
      - `brandContent` boolean
      - `brandOrganic` boolean
      - `isAigc` boolean
    - `instagram` object: Choose Story explicitly. The default is a feed photo or Reel. Stories support both Instagram Login and Facebook Login, subject to Meta account eligibility. Facebook Login requires an Instagram Business account linked to a Facebook Page.
      - `placement` 'feed' | 'story' | 'stories': story and stories are equivalent. Exactly one JPEG or PNG image, or MP4/MOV video. PostLake converts PNG to JPEG for Instagram. Story text is not displayed.
      - `shareToFeed` boolean: Reels only; rejected for Stories.
    - `facebook` object: Facebook Page publishing options. Page mentions only resolve after Meta approves PostLake's Page Mentioning feature.
      - `link` string, uri: URL shown as a link preview on text posts.
      - `locationId` string: Facebook Page or place ID to tag as the post location.
      - `mentionPageIds` string: Comma-separated Facebook Page IDs. PostLake appends Facebook's inline @[Page-ID] tokens to the caption. Meta resolves them as Page links only after Page Mentioning approval.
      - `mentionPages` object[]: Selected Facebook Pages already written into text as @PageName. PostLake replaces each matching display name with Facebook's @[Page-ID] token at the same position.
        - `id` string, required: Numeric Facebook Page ID.
        - `name` string, required: Visible Page name without the leading @.

## Response `201`

Created

- 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`: Invalid input
- `401`: Unauthorized
- `402`: Entitlement exceeded
- `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)
