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

# Create a draft post

`POST /posts`

## Request

- Base URL: `https://www.juzpost.com/api/cli/v1`
- URL: `https://www.juzpost.com/api/cli/v1/posts`
- Auth: HTTP bearer

Creates a post in `draft` status. Nothing is published or scheduled by this call. Use
`POST /posts/{postId}/schedule` or `POST /schedule` afterwards. Upload media first via
`POST /storage/presign` and pass the returned `resolvedKey` values in `mediaUrls`;
referencing a key here also claims it so the orphan-upload sweeper will not delete it.
Only the returned `id` comes back; read the post with `GET /posts/{postId}`.

## Request body

- CreatePostRequest: Fields for a new draft. Every field is optional; an empty body creates an empty draft of type `video`. Scheduling is a separate call.
  - `content` string: The post body or caption, up to 2200 characters. Defaults to an empty string. Per-platform limits are enforced later, at schedule time.
  - `mediaUrls` string[]: Storage keys of already-uploaded media, using the `resolvedKey` values returned by `POST /storage/presign`. Listing a key here claims it so it will not be swept away as an orphan. These are storage keys, not public URLs.
  - `title` string: Title stored in the post's metadata. Note that scheduling re-validates titles against a 100-character limit, so keep it under that if the post will be scheduled.
  - `description` string: Long-form description stored in the post's metadata, used by platforms that have a description field separate from the caption.
  - `hashtags` string[]: Hashtags stored in the post's metadata. Do not include the leading `#`.
  - `type` 'text' | 'image' | 'video' | 'story' | 'blog': What kind of content the post carries. Anything other than `text` and `blog` must have at least one media key before it can be scheduled. `blog` is a WordPress blog post: `title` is required and `description` is the body, HTML sent as written (plain text works, blank lines become paragraphs). The first image is the featured image and any others are added under the body. Images only.
  - `coverR2Key` string: Storage key of a cover image already uploaded via `POST /storage/presign`. Referencing it here also claims it against the orphan sweeper.

## Response `201`

Draft created.

- CreatedPost: The id of the post that was created or updated. Fetch `GET /posts/{postId}` if you need the full record.
  - `id` string, uuid, required: The post id.

## Other responses

- `400`: The request body or query string failed validation. `error.message` names the first problem found.
- `401`: No bearer token was sent, or the token is unknown, revoked or malformed. Run the device login again to obtain a new token.
- `402`: The plan behind this token does not include the capability this operation needs. `error.details.requiredPlan` names the tier that does. This is never a token problem, so do not retry without changing the plan.
- `429`: This workspace has already created 30 posts through the API or an AI connector in the last hour. `error.details` is `{ "limit": 30, "windowSeconds": 3600 }`. Wait for the window to roll over, or schedule the drafts you already have. Posts created in the dashboard do not count.

---

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