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

# Create a post

`POST /posts`

## Request

- Base URL: `https://api.deploy.social/v1`
- URL: `https://api.deploy.social/v1/posts`
- Auth: HTTP bearer

Creates a draft, unless you send `publish_at`. A draft comes back with `issues` (anything that still needs fixing) and a `review_url` that opens it in Deploy. With `publish_at` it goes out now or at that time, and the video has to be `ready`.

## Headers

- `Idempotency-Key` string: Any unique string, like a UUID. Send the same request again with the same key within 24 hours and you get the first response back instead of a second post.

## Request body

- CreatePost
  - `kind` 'video' | 'photos' | 'text': `video`, `photos` or `text`. Defaults to `video`. New kinds may be added.
  - `media_id` string: The video to post. Required when `kind` is `video` (the default); not used otherwise.
  - `media` PostMediaInput[]: The photos to post, in order, 1 to 35. Required when `kind` is `photos`; not used otherwise.
    - `media_id` string, required: The video's id. Starts with `media_`.
    - `alt_text` string: Alt text for this photo, where the platform takes it.
  - `caption` string: The caption every destination uses unless it has its own. Required for a video post, up to 2,200 characters; optional for photos and text, up to 65,536 (the platform's own limit, checked when you post, may be shorter).
  - `title` string: YouTube's title. Video only.
  - `description` string: YouTube's description. Video only.
  - `tags` string[]: Tags, without the #. YouTube only, video only.
  - `first_comment` string: Posted as the first comment where the platform takes one. Video only.
  - `notify_email` boolean: Email the workspace when the post has gone out.
  - `destinations` DestinationInput[], required: Where to post it.
    - `destination_id` string, required: The destination's id. Starts with `dest_`.
    - `caption` string, nullable: A caption for this destination only.
    - `title` string, nullable: YouTube only.
    - `description` string, nullable: YouTube only.
    - `tags` string[], nullable: Tags, without the #. YouTube only.
    - `first_comment` string, nullable: A first comment for this destination, where the platform takes one.
    - `options` object: Settings for this platform, by name. See the destination's options. Anything you leave out uses its default. For a `text` post on X, Bluesky or Threads, `thread: true` splits a too-long caption into a thread instead of refusing it. For a `photos` post on X or Bluesky, `photo_positions` (a list of positions from `media`, as strings, like `["0","2"]`) picks which photos go out there when you sent more than the platform takes.
  - `publish_at` string: When to post: `"now"`, or a time with a timezone (like `2026-11-30T09:00:00-07:00`), up to a year ahead.

## Response `201`

The new post.

- Post
  - `id` string, required: The post's id. Starts with `post_`.
  - `status` string, required: `draft`, `scheduled`, `publishing`, `published`, `partially_published`, `failed` or `cancelled`. New values may be added.
  - `kind` string, required: `video`, `photos` or `text`. New values may be added.
  - `media_id` string, nullable, required: The video's id. Starts with `media_`.
  - `media` PostMediaItem[], required: The photos, in order. Empty for `video` and `text` posts.
    - `media_id` string, nullable, required: The photo's id. Starts with `media_`.
    - `position` integer, required: Where this photo sits in the post, starting at 0.
    - `alt_text` string, nullable, required
  - `caption` string, required
  - `title` string, nullable, required
  - `description` string, nullable, required
  - `tags` string[], nullable, required
  - `first_comment` string, nullable, required
  - `scheduled_at` string, date-time, nullable, required: When it's set to go out.
  - `notify_email` boolean, required
  - `created_via` string, required: Where it was made: `app` (the web app), `mobile` (the phone app), `api` or `mcp`.
  - `created_at` string, date-time, required: When it was created.
  - `updated_at` string, date-time, required: When it last changed.
  - `destinations` PostDestination[], required
    - `destination_id` string, required: The destination's id. Starts with `dest_`.
    - `platform` string, required
    - `handle` string, required
    - `caption` string, nullable, required: This destination's own caption. `null` means it uses the post's.
    - `title` string, nullable, required
    - `description` string, nullable, required
    - `tags` string[], nullable, required
    - `first_comment` string, nullable, required
    - `options` object, required: The settings this destination posts with, defaults included.
    - `status` string, required: `pending`, `publishing`, `published`, `failed` or `cancelled`. New values may be added.
    - `progress` object, nullable, required
      - `phase` string, nullable, required: What's happening right now, like `uploading` or `waiting`. For display only.
      - `percent` integer, nullable, required
    - `url` string, nullable, required: The post on the platform, once it's live.
    - `platform_post_id` string, nullable, required
    - `published_at` string, date-time, nullable, required: When it went live here.
    - `warning` string, nullable, required: It posted, but something didn't go as planned (a first comment that couldn't be added, say).
    - `failure` object, nullable, required
      - `category` string, required: `user`, `platform`, `internal` or `unknown`. New values may be added.
      - `code` string, nullable, required: The platform's own code for it, when there is one.
      - `message` string, required: What happened and what to do, in a sentence you can show a person.
      - `action` string, required: `reconnect`: reconnect the account in Deploy, then retry. `retry`: try again. `none`: retrying won't help.
  - `review_url` string: Drafts only: opens the draft in Deploy, to check it over and post it from there.
  - `issues` PostIssue[]: Drafts only: what still needs fixing before it can go out.
    - `path` string, required
    - `code` string, required
    - `message` string, required
    - `severity` 'error' | 'warning', required: `error` has to be fixed before the post can go out. `warning` won't stop it.

## Other responses

- `400`: The request body isn't valid JSON, or a header is malformed.
- `401`: The API key is missing, wrong, revoked or expired: `missing_api_key`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`.
- `403`: The key can't do this: `insufficient_scope` (a read-only key), `plan_required`, `usage_limit_exceeded` (this month's videos are used up), `access_revoked` (its creator left the workspace), `workspace_archived`.
- `409`: The post or video isn't in a state that allows this right now: `invalid_state` (detail code `media_expired` when the video's files were removed; upload it again), `media_not_ready` (with `Retry-After`), `idempotency_key_in_progress`.
- `422`: Something in the request needs fixing. `details` lists each problem and where it is: `validation_failed`, or `idempotency_key_reused` when a key is sent again with a different request.
- `429`: Slow down and wait `Retry-After` seconds. Either this key has used its requests for the minute (Standard: 60 reads and 20 writes, Studio: 120 and 30), or, when publishing or scheduling, the plan's API posts for the day are used (detail code `daily_post_limit`; Standard: 30 a day across your account, Studio: 100 a day per workspace; `Retry-After` runs to midnight UTC). Posts made in the app never count.
- `500`: Something broke on our side. Retry, and contact us with the request id if it keeps happening.
- `503`: We can't take the request right now. Retry shortly.

---

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