---
title: "Schedule a post"
method: POST
path: "/posts"
tags: ["Posts"]
---

# Schedule a post

`POST /posts`

## Request

- Base URL: `https://api.postplanify.com/api/v1`
- URL: `https://api.postplanify.com/api/v1/posts`
- Auth: HTTP bearer

Creates and schedules a post to a social account. The post is added to the publish queue and will be automatically published at the specified `scheduledAt` time.

## Workflow

1. **Upload media**: `POST /media/upload` for each file. Save the returned `id` values.
2. **Create the post**: call this endpoint with `mediaIds`, `scheduledAt`, and platform params.
3. **Done**: the post is queued and will publish automatically at the scheduled time.

**Media ordering:** For carousel posts (Instagram, TikTok, etc.), files appear in the order of the `mediaIds` array. List the IDs in the order you want them to appear.

## Media rules by platform

| Platform | Media required? | Limits | Notes |
|----------|----------------|--------|-------|
| Instagram | Yes | 1–10 images/videos | Stories: exactly 1 |
| Facebook | No | Up to 10 images OR 1 video | Cannot mix types. Stories: exactly 1 |
| X | No | Up to 4 images/videos | Text-only posts allowed |
| YouTube | Yes | Exactly 1 video | `youtubeParams.title` is required |
| TikTok | Yes | 1 video OR 1+ images | Cannot mix types |
| LinkedIn | No | Up to 20 images OR 1 video OR 1 PDF | Cannot mix types |
| Pinterest | Yes | Exactly 1 image or video | `pinterestParams.boardId` is required |
| Threads | No | Up to 20 images/videos | Text-only posts allowed |
| Bluesky | No | Up to 4 images OR 1 video | Cannot mix types |
| Google Business | No | Up to 1 image | No video via API. Text-only posts allowed |

## Platform-specific parameters

Some platforms require or accept additional parameters. Pass them only for their matching platform: sending `youtubeParams` on an Instagram post will return a validation error.

- **YouTube**: `youtubeParams` required: `title` (max 100 chars), optional `categoryId` (default "22"), `tags` (comma-separated, max 500 chars), `selfDeclaredMadeForKids`
- **TikTok**: `tiktokParams` optional: `privacyLevel` (defaults to `PUBLIC_TO_EVERYONE`), `disableDuet`, `disableComment`, `disableStitch`, `isDraftType`, `isCommercialContent`, `isYourBrand`, `isBrandedContent`, `autoAddMusic`, `photosPostTitle` (max 90 chars, photo posts only)
- **Pinterest**: `pinterestParams` required: `boardId` (get IDs from `GET /social-accounts/{id}/pinterest-boards`), optional `title` (max 100 chars), `link` (valid URL), `altText` (max 500 chars)
- **Google Business**: `googleBusinessParams` optional: `topicType` (default `STANDARD`; `EVENT` and `OFFER` require `eventTitle`, `startDate`, `endDate`), `actionType` (CTA button, not for OFFER), `actionUrl` (required for non-CALL CTA), `couponCode` (OFFER only, max 58 chars), `redeemOnlineUrl`, `termsConditions`
- **X**: `xCommunityId` optional, `xTaggedUsers` optional (comma-separated usernames, max 10, requires media)

## Link shortening

Set `shortenLinks: true` to shorten the links in a post with the workspace's Bitly account. Bitly is connected per workspace in the dashboard under Settings > Integrations. These fields work on every platform.

- Links in the caption, thread posts and the first comment are shortened when the post is published, not when it is created, so the stored caption keeps the original links until then.
- Each link gets `utm_source` (the platform, e.g. `instagram`) and `utm_medium=social`, plus `utm_campaign` if you set `utmCampaign`. Because `utm_source` differs per platform, each platform gets its own short link and its own click count in Bitly.
- Links that are already Bitly links are left as they are.
- If the workspace has no Bitly connected, or Bitly fails (for example the monthly link limit is reached), the post is still published with the original links and a warning is added to the post's logs.

## Caption limits

Caption length limits are enforced by each platform at publish time:

| Platform | Max caption length |
|----------|--------------------|
| Instagram | 2,200 characters |
| TikTok | 2,200 characters |
| Threads | 500 characters |
| Pinterest | 500 characters (description) |
| Bluesky | 300 characters |
| Google Business | 1,500 characters |
| Facebook, X, YouTube, LinkedIn | No strict limit |

## Stories

Set `isStory: true` to post as a story. Only Instagram and Facebook support stories. Stories require exactly 1 media file.

## Example

```json
{
  "workspaceId": "d35f525f-63e6-40ae-8946-8c120edb0225",
  "socialAccountId": "c47eb231-8eb2-4600-ac24-6951c7b5e3ce",
  "caption": "Check out our latest product launch!",
  "scheduledAt": "2026-03-15T20:00:00.000Z",
  "mediaIds": ["e1a2b3c4-d5e6-f7a8-b9c0-d1e2f3a4b5c6"]
}
```

## Request body

- CreatePostRequest
  - `workspaceId` string, uuid, required: The workspace this post belongs to
  - `socialAccountId` string, uuid, required: The social account to publish to
  - `caption` string: Post text/caption. Length limits vary by platform (e.g. Instagram 2200, Threads 500, Bluesky 300).
  - `scheduledAt` string, date-time, required: When to publish (UTC, must be in the future)
  - `mediaIds` string[]: Media IDs to attach, from `GET /media` (API uploads or the workspace's dashboard library) or `POST /media/upload`. Library media must belong to this post's `workspaceId`. Requirements vary by platform.
  - `isStory` boolean: Post as a story (Instagram and Facebook only, requires exactly 1 media file)
  - `youtubeParams` YouTubeParams: YouTube-specific parameters. Only applicable to YouTube posts.
    - `title` string, required: Video title (required)
    - `selfDeclaredMadeForKids` boolean: Whether the video is made for kids (COPPA compliance)
    - `categoryId` '1' | '2' | '10' | '15' | '17' | '18' | '19' | '20' | '21' | '22' | '23' | '24' | '25' | '26' | '27' | '28' | '29' | '30' | '31' | '32' | '33' | '34' | '35' | '36' | '37' | '38' | '39' | '40' | '41' | '42' | '43' | '44': YouTube video category ID. Defaults to "22" (People & Blogs). Common categories: "1" Film & Animation, "2" Autos & Vehicles, "10" Music, "15" Pets & Animals, "17" Sports, "19" Travel & Events, "20" Gaming, "22" People & Blogs, "23" Comedy, "24" Entertainment, "25" News & Politics, "26" Howto & Style, "27" Education, "28" Science & Technology.
    - `tags` string: Comma-separated tags (e.g. "tech,tutorial,howto")
  - `tiktokParams` TikTokParams: TikTok-specific parameters. Only applicable to TikTok posts.
    - `privacyLevel` 'SELF_ONLY' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'PUBLIC_TO_EVERYONE': Who can view the post. Defaults to `PUBLIC_TO_EVERYONE`.
    - `disableDuet` boolean: Prevent others from creating duets with this video
    - `disableComment` boolean: Disable comments on the post
    - `disableStitch` boolean: Prevent others from stitching this video
    - `isDraftType` boolean: Save as draft in TikTok inbox instead of publishing directly
    - `isCommercialContent` boolean: Disclose the post as commercial content
    - `isYourBrand` boolean: The post promotes your own brand or business
    - `isBrandedContent` boolean: The post is a paid partnership or sponsored content. Cannot be `true` when `privacyLevel` is `SELF_ONLY`.
    - `autoAddMusic` boolean: Automatically add music to photo posts
    - `photosPostTitle` string: Title for photo/carousel posts only (max 90 characters)
  - `pinterestParams` PinterestParams: Pinterest-specific parameters. Only applicable to Pinterest posts.
    - `title` string: Pin title
    - `boardId` string, required: Target board ID (required). Use `GET /social-accounts/{id}/pinterest-boards` to list available boards and their IDs.
    - `boardName` string: Board name: stored for reference, auto-filled from the selected board
    - `link` string: Destination URL when the pin is clicked. Must be a valid URL (contain a dot, no spaces).
    - `altText` string: Alt text for accessibility (max 500 characters)
  - `googleBusinessParams` GoogleBusinessParams: Google Business Profile post parameters. Only applicable to Google Business posts. Three post types are supported: - **STANDARD**: General update or announcement (default) - **EVENT**: Promote an event with dates. Requires `eventTitle`, `startDate`, and `endDate`. - **OFFER**: Share a deal or promotion. Requires `eventTitle`, `startDate`, and `endDate`. Automatically includes a "View offer" button: do not set `actionType`.
    - `topicType` 'STANDARD' | 'EVENT' | 'OFFER': Post type
    - `actionType` 'BOOK' | 'ORDER' | 'SHOP' | 'LEARN_MORE' | 'SIGN_UP' | 'CALL', nullable: CTA button type. Not supported for OFFER posts (they get an automatic "View offer" button). CALL uses the business profile phone number and does not require `actionUrl`.
    - `actionUrl` string, nullable: CTA button URL. Required when `actionType` is set (except CALL).
    - `eventTitle` string, nullable: Event or offer title (max 58 characters). Required for EVENT and OFFER posts.
    - `startDate` string, date-time, nullable: Event/offer start date (ISO 8601). Required for EVENT and OFFER posts. Must be before `endDate`.
    - `endDate` string, date-time, nullable: Event/offer end date (ISO 8601). Required for EVENT and OFFER posts. Must be after `startDate`.
    - `couponCode` string, nullable: Coupon code for OFFER posts (max 58 characters)
    - `redeemOnlineUrl` string, nullable: Redemption URL for OFFER posts
    - `termsConditions` string, nullable: Terms and conditions for OFFER posts
  - `xCommunityId` string: X community ID (X posts only)
  - `xTaggedUsers` string: Comma-separated X usernames to tag in media (X posts only, max 10)
  - `shortenLinks` boolean: Shorten links in the caption, thread posts and first comment with the workspace's connected Bitly account when the post is published. Ignored if the workspace has no Bitly connected (the original links are posted). See "Link shortening" under Create post.
  - `utmCampaign` string, nullable: Value for `utm_campaign` added to links when `shortenLinks` is true. `utm_source` (the platform) and `utm_medium=social` are added automatically.

## Response `201`

Post created and scheduled

- object
  - `ok` boolean
  - `data` Post
    - `id` string, uuid
    - `workspaceId` string, uuid
    - `caption` string, nullable
    - `status` 'SCHEDULED' | 'PUBLISHED' | 'CANCELLED' | 'RESCHEDULED'
    - `platform` 'FACEBOOK' | 'INSTAGRAM' | 'X' | 'LINKEDIN' | 'TIKTOK' | 'YOUTUBE' | 'THREADS' | 'BLUESKY' | 'PINTEREST' | 'GOOGLE_BUSINESS'
    - `socialPlatformPostId` string, nullable: The native post ID on the social platform
    - `isStory` boolean
    - `shortenLinks` boolean: Whether links are shortened with the workspace's Bitly account at publish time
    - `utmCampaign` string, nullable: The `utm_campaign` added to shortened links, if any
    - `scheduledAt` string, date-time, nullable
    - `publishedAt` string, date-time, nullable
    - `createdAt` string, date-time
    - `updatedAt` string, date-time
    - `socialAccount` SocialAccountSummary
      - `id` string, uuid
      - `platform` 'FACEBOOK' | 'INSTAGRAM' | 'X' | 'LINKEDIN' | 'TIKTOK' | 'YOUTUBE' | 'THREADS' | 'BLUESKY' | 'PINTEREST' | 'GOOGLE_BUSINESS'
      - `accountName` string
      - `accountUrl` string, nullable
      - `profilePictureUrl` string, nullable
      - `customLabel` string, nullable: User-set label to distinguish accounts (e.g. "Downtown")
    - `medias` PostMedia[]
      - `id` string, uuid
      - `name` string
      - `url` string
      - `type` string: Media type (e.g. IMAGE_JPEG, VIDEO_MP4)
      - `size` number: File size in bytes
      - `width` integer, nullable
      - `height` integer, nullable
      - `duration` number, nullable: Video duration in seconds
      - `coverImageUrl` string, nullable
    - `logs` PostLog[]
      - `type` 'SUCCESS' | 'ERROR' | 'WARNING' | 'INFO'
      - `message` string, nullable
      - `createdAt` string, date-time
    - `platformParams` object, nullable: Platform-specific parameters (YouTube, TikTok, Pinterest, or Google Business fields)
    - `label` PostLabel, nullable: A user-defined content label attached to a post (used for organizing content pillars)
      - `name` string
      - `color` string: Hex color code
    - `metrics` PostMetrics, nullable: A post's most recent daily analytics snapshot. Snapshots are collected once per day per post, so `measuredAt` shows how fresh these figures are.
      - `views` integer, nullable
      - `likes` integer, nullable
      - `comments` integer, nullable
      - `shares` integer, nullable
      - `engagementRate` number, float, nullable: Engagement as a share of views, where the platform reports it
      - `platformMetrics` object, nullable: Platform-specific extras, present only where the platform reports them. For example X returns bookmarks and quotes, Instagram returns reach and saves.
      - `measuredAt` string, date: The date this snapshot was taken

## Other responses

- `400`: Validation error (missing fields, invalid media, wrong platform params)
- `403`: Workspace requires an active subscription
- `404`: Workspace not found
- `429`: Too many posts scheduled for this social account in the same time window (max 6/hour, 100/day)

---

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