---
title: "Create a draft, schedule, or publish now (controlled by status)"
method: POST
path: "/posts"
tags: ["Posts"]
---

# Create a draft, schedule, or publish now (controlled by status)

`POST /posts`

## Request

- Base URL: `https://api.posteahora.com/functions/v1/api`
- URL: `https://api.posteahora.com/functions/v1/api/posts`
- Auth: HTTP bearer
- TypeScript: <https://skmtc.dev/posteahora/apis/posteahora-public-api/docs/posts/post.ts> (a function with typed inputs; its response is checked against the schema)

Multi-platform posts fan out into one row per platform at publish time. Publishing is asynchronous — a "published" create returns status "queued"; poll GET /posts/{id} and read platform_results.

## Request body

- CreatePostRequest
  - `title` string
  - `caption` string: Shared caption. Per-network override via platformCaptions.
  - `accountMappings` AccountMapping[], required: Canonical channel selection. Prefer this over platforms+accountId.
    - `platform` string, required
    - `accountId` string: connected_accounts.id. Required for every network except a few that don't need account selection.
  - `platforms` string[]: Convenience form for a single-channel post (combine with accountId).
  - `accountId` string: Used only with the platforms convenience form.
  - `mediaUrls` string[]
  - `mediaType` 'image' | 'video'
  - `postType` 'post' | 'reel' | 'story'
  - `hashtags` string[]
  - `platformCaptions` object: Per-platform caption/description override, keyed by platform id.
  - `platformOptions` PlatformOptions: Per-network settings, keyed by platform id.
    - `youtube` YouTubeOptions: The video description is the post caption (platformCaptions.youtube, ≤5000 chars). The video title is the required `title` field. There is no "Shorts" flag — a vertical clip ≤3 min is auto-classified as a Short.
      - `title` string, required
      - `categoryId` string, required
      - `madeForKids` boolean, required
      - `privacyStatus` 'public' | 'unlisted' | 'private'
      - `tags` string[]
      - `defaultLanguage` string
      - `containsSyntheticMedia` boolean
      - `embeddable` boolean
      - `publicStatsViewable` boolean
      - `license` 'youtube' | 'creativeCommon'
      - `thumbnailUrl` string, uri
    - `tiktok` TikTokOptions
      - `privacyLevel` 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY', required
      - `disableComment` boolean, required
      - `brandContentToggle` boolean, required
      - `brandOrganicToggle` boolean, required
      - `isAigc` boolean, required
      - `postMode` 'DIRECT_POST' | 'MEDIA_UPLOAD'
      - `disableDuet` boolean: video only
      - `disableStitch` boolean: video only
      - `videoCoverTimestampMs` number: video only
      - `autoAddMusic` boolean: photo only
      - `photoCoverIndex` number: photo only
      - `photoTitle` string: photo only
    - `instagram` InstagramOptions: Set postType "reel" (top level) for a Reel, "story" for a Story.
      - `shareToFeed` boolean
      - `coverUrl` string, uri
      - `thumbOffsetMs` number
      - `audioName` string
      - `collaborators` string[]
      - `locationId` string
      - `userTags` object[]
        - `username` string, required
        - `x` number
        - `y` number
      - `altText` string
      - `isAiGenerated` boolean
      - `isTrialReel` boolean: reel only
      - `trialGraduationStrategy` 'MANUAL' | 'SS_PERFORMANCE': reel only
    - `threads` ThreadsOptions
      - `replyControl` 'everyone' | 'accounts_you_follow' | 'mentioned_only' | 'parent_post_author_only' | 'followers_only'
      - `topic` string
      - `altText` string
      - `linkAttachment` string, uri
      - `quotePostId` string
      - `locationId` string
      - `allowlistedCountryCodes` string[]
      - `chainItems` object[]
        - `text` string, required
        - `mediaUrl` string, uri
        - `mediaType` 'image' | 'video'
        - `altText` string
    - `bluesky` BlueskyOptions
      - `langs` string[]
    - `mastodon` MastodonOptions
      - `visibility` 'public' | 'unlisted' | 'private' | 'direct'
      - `spoilerText` string
      - `sensitive` boolean
      - `language` string
    - `telegram` TelegramOptions
      - `disableNotification` boolean
      - `protectContent` boolean
    - `discord` DiscordOptions
      - `channelId` string
    - `pinterest` PinterestOptions: boardId is REQUIRED and validated on create (400 without it). A Pin needs at least one image; 2–5 images make a carousel.
      - `boardId` string, required: Board id from GET /accounts/{id}/boards
      - `boardName` string: Display only
      - `title` string
      - `link` string, uri
      - `altText` string
      - `coverUrl` string, uri: Video Pins: 2:3 cover image URL
      - `thumbOffsetMs` integer: Video Pins: keyframe used as cover when coverUrl is absent (ms)
  - `status` 'draft' | 'scheduled' | 'published'
  - `scheduledAt` string, date-time: Required (and must be in the future) when status = scheduled.

## Response `201`

Created

- CreatePostResult
  - `postIds` string[]
  - `status` 'draft' | 'scheduled' | 'queued'
  - `dispatchDeferred` boolean: When true, rows are stored but the worker wasn't reached synchronously; the cron sweep will publish them.

## Other responses

- `400`: Invalid request
- `402`: Monthly post quota reached
- `403`: Key lacks the required scope (or viewer-role key attempting a write)

---

[API](https://skmtc.dev/posteahora/apis/posteahora-public-api.md) - [All operations](https://skmtc.dev/posteahora/apis/posteahora-public-api/llms.txt) - [OpenAPI document](https://skmtc.dev/posteahora/apis/posteahora-public-api/revisions/552a8eb6c30f?raw)
