---
title: "/posts"
method: POST
path: "/projects/{project_id}/posts"
tags: ["Posts"]
---

# /posts

`POST /projects/{project_id}/posts`

## Request

- Base URL: `https://api.productbyte.com/v1`
- URL: `https://api.productbyte.com/v1/projects/{project_id}/posts`
- Auth: HTTP bearer

Creates a post from an existing creative. A post targets one channel, so naming several channels creates several posts and the response is always a list.

Set `publish_type` to decide what happens: `draft` saves it, `scheduled` needs a future `scheduled_at`, and `immediate` publishes now and rejects a timestamp. It is required — there is no default, because this endpoint can publish.

Publishing is asynchronous and `scheduled_at` is a *not before* time, not an exact one: posts typically go out within a few seconds of it. Do not build a countdown that assumes the second.

Send an `Idempotency-Key` header on this endpoint. Publishing is not reversible, and a retry after a timeout would otherwise post twice. Reuse the same key when you retry: if the first request finished you get its original response back, and if it is still running you get `409` `API_IDEMPOTENCY_KEY_IN_FLIGHT` — wait a moment and retry again rather than sending a new key. Retrying with the same key is safe in every ordinary case, but it is a best-effort guard rather than a guarantee: two requests fired at the same instant can still both go through, so retry in sequence rather than in parallel.

Requires the `posts:write` scope.

## Path parameters

- `project_id` string, required: The project to create the post in.

## Request body

- union
  - object
    - `creative_id` string, required: A creative id from `/creatives/upload`.
    - `format_ids` string[], required: Which of the creative’s formats to publish. Ids come from its `formats`.
    - `channels` object[], required: Channels to publish to, from `/channels`, each with optional settings. One post is created per channel.
      - `id` string, required: A channel id from `/channels`.
      - `settings` object: How this post publishes on this channel.
        - `link_url` string: Destination URL. X shows it as a call to action on media and a Pin opens it; other channels ignore it.
        - `meta` object: Instagram and Facebook Page settings. A Reel cover and per-image alt text are set in ProductByte rather than here: both are keyed by internal asset ids this API does not expose.
          - `mode` 'feed_post' | 'story_post' | 'reel_post', required: Which Instagram surface to publish to. A Facebook Page takes `feed_post` only. A single video on `feed_post` publishes as a Reel — Instagram has no other container for it.
          - `title` string: Headline for a Facebook Page video. Ignored elsewhere.
          - `share_to_feed` boolean: Reels only. `false` keeps the Reel out of your profile feed, leaving it in the Reels tab.
          - `trial_reel` boolean: Reels only. Publishes to non-followers first. Accounts Instagram finds ineligible publish an ordinary Reel instead, reported back on the post.
          - `ai_generated` boolean: Discloses AI use. Instagram requires it for photorealistic video.
          - `collaborators` string[]: Up to 3 public Instagram usernames, without the leading @, invited to co-author. Instagram does not allow these on a trial reel or a Story.
        - `tiktok` object: TikTok settings.
          - `title` string: Title for the TikTok post. At most 90 characters.
          - `direct_post` object: Publishes straight to TikTok instead of sending the video to the creator’s inbox. Only on a `draft`. TikTok requires the creator to see its consent terms — the Branded Content Policy and Music Usage Confirmation — and to choose from the privacy options their account currently offers. An API call cannot show them either, so a Direct Post is prepared here and published from ProductByte, where those screens exist. Sending this with `publish_type` of `scheduled` or `immediate` is refused.
            - `privacy_level` 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY', required: Who can see the post. Required, and must be one the creator currently allows.
            - `allow_comment` boolean, required: Whether viewers may comment.
            - `allow_duet` boolean: Video only.
            - `allow_stitch` boolean: Video only.
            - `auto_add_music` boolean: Photo posts only.
            - `is_aigc` boolean: Discloses AI-generated content.
            - `your_brand` boolean: The post promotes your own brand.
            - `branded_content` boolean: The post promotes a third party. Cannot be combined with `SELF_ONLY`.
        - `pinterest` object: Pinterest settings. Required on a Pinterest channel: every Pin goes on a board.
          - `board_id` string, required: The board the Pin goes on, one of the account’s boards.
          - `title` string: The Pin title. At most 100 characters.
          - `alt_text` string: What the picture shows, for screen readers. At most 500 characters.
    - `campaign_id` string: Group the posts under this campaign, from `/campaigns`. Omit to leave them unassigned — they can be assigned later in ProductByte.
    - `copy` string, required: The text published alongside the media.
    - `replies` object[]: Comments to publish under the post once it is live, in order. Not every channel supports them.
      - `copy` string, required: The comment text. Must not be empty.
      - `delay_seconds` integer: Wait after the previous item publishes, in seconds. At most 30 days.
    - `auto_delete_after_seconds` integer: Remove the post from the channel this long after it publishes. Whole minutes, 60 seconds minimum. Not supported on Instagram or TikTok.
    - `publish_type` 'draft', required
    - `scheduled_at` string, date-time: An intended time. Nothing publishes.
  - object
    - `creative_id` string, required: A creative id from `/creatives/upload`.
    - `format_ids` string[], required: Which of the creative’s formats to publish. Ids come from its `formats`.
    - `channels` object[], required: Channels to publish to, from `/channels`, each with optional settings. One post is created per channel.
      - `id` string, required: A channel id from `/channels`.
      - `settings` object: How this post publishes on this channel.
        - `link_url` string: Destination URL. X shows it as a call to action on media and a Pin opens it; other channels ignore it.
        - `meta` object: Instagram and Facebook Page settings. A Reel cover and per-image alt text are set in ProductByte rather than here: both are keyed by internal asset ids this API does not expose.
          - `mode` 'feed_post' | 'story_post' | 'reel_post', required: Which Instagram surface to publish to. A Facebook Page takes `feed_post` only. A single video on `feed_post` publishes as a Reel — Instagram has no other container for it.
          - `title` string: Headline for a Facebook Page video. Ignored elsewhere.
          - `share_to_feed` boolean: Reels only. `false` keeps the Reel out of your profile feed, leaving it in the Reels tab.
          - `trial_reel` boolean: Reels only. Publishes to non-followers first. Accounts Instagram finds ineligible publish an ordinary Reel instead, reported back on the post.
          - `ai_generated` boolean: Discloses AI use. Instagram requires it for photorealistic video.
          - `collaborators` string[]: Up to 3 public Instagram usernames, without the leading @, invited to co-author. Instagram does not allow these on a trial reel or a Story.
        - `tiktok` object: TikTok settings.
          - `title` string: Title for the TikTok post. At most 90 characters.
          - `direct_post` object: Publishes straight to TikTok instead of sending the video to the creator’s inbox. Only on a `draft`. TikTok requires the creator to see its consent terms — the Branded Content Policy and Music Usage Confirmation — and to choose from the privacy options their account currently offers. An API call cannot show them either, so a Direct Post is prepared here and published from ProductByte, where those screens exist. Sending this with `publish_type` of `scheduled` or `immediate` is refused.
            - `privacy_level` 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY', required: Who can see the post. Required, and must be one the creator currently allows.
            - `allow_comment` boolean, required: Whether viewers may comment.
            - `allow_duet` boolean: Video only.
            - `allow_stitch` boolean: Video only.
            - `auto_add_music` boolean: Photo posts only.
            - `is_aigc` boolean: Discloses AI-generated content.
            - `your_brand` boolean: The post promotes your own brand.
            - `branded_content` boolean: The post promotes a third party. Cannot be combined with `SELF_ONLY`.
        - `pinterest` object: Pinterest settings. Required on a Pinterest channel: every Pin goes on a board.
          - `board_id` string, required: The board the Pin goes on, one of the account’s boards.
          - `title` string: The Pin title. At most 100 characters.
          - `alt_text` string: What the picture shows, for screen readers. At most 500 characters.
    - `campaign_id` string: Group the posts under this campaign, from `/campaigns`. Omit to leave them unassigned — they can be assigned later in ProductByte.
    - `copy` string, required: The text published alongside the media.
    - `replies` object[]: Comments to publish under the post once it is live, in order. Not every channel supports them.
      - `copy` string, required: The comment text. Must not be empty.
      - `delay_seconds` integer: Wait after the previous item publishes, in seconds. At most 30 days.
    - `auto_delete_after_seconds` integer: Remove the post from the channel this long after it publishes. Whole minutes, 60 seconds minimum. Not supported on Instagram or TikTok.
    - `publish_type` 'scheduled', required
    - `scheduled_at` string, date-time, required: When to publish. Required, and must be in the future.
  - object
    - `creative_id` string, required: A creative id from `/creatives/upload`.
    - `format_ids` string[], required: Which of the creative’s formats to publish. Ids come from its `formats`.
    - `channels` object[], required: Channels to publish to, from `/channels`, each with optional settings. One post is created per channel.
      - `id` string, required: A channel id from `/channels`.
      - `settings` object: How this post publishes on this channel.
        - `link_url` string: Destination URL. X shows it as a call to action on media and a Pin opens it; other channels ignore it.
        - `meta` object: Instagram and Facebook Page settings. A Reel cover and per-image alt text are set in ProductByte rather than here: both are keyed by internal asset ids this API does not expose.
          - `mode` 'feed_post' | 'story_post' | 'reel_post', required: Which Instagram surface to publish to. A Facebook Page takes `feed_post` only. A single video on `feed_post` publishes as a Reel — Instagram has no other container for it.
          - `title` string: Headline for a Facebook Page video. Ignored elsewhere.
          - `share_to_feed` boolean: Reels only. `false` keeps the Reel out of your profile feed, leaving it in the Reels tab.
          - `trial_reel` boolean: Reels only. Publishes to non-followers first. Accounts Instagram finds ineligible publish an ordinary Reel instead, reported back on the post.
          - `ai_generated` boolean: Discloses AI use. Instagram requires it for photorealistic video.
          - `collaborators` string[]: Up to 3 public Instagram usernames, without the leading @, invited to co-author. Instagram does not allow these on a trial reel or a Story.
        - `tiktok` object: TikTok settings.
          - `title` string: Title for the TikTok post. At most 90 characters.
          - `direct_post` object: Publishes straight to TikTok instead of sending the video to the creator’s inbox. Only on a `draft`. TikTok requires the creator to see its consent terms — the Branded Content Policy and Music Usage Confirmation — and to choose from the privacy options their account currently offers. An API call cannot show them either, so a Direct Post is prepared here and published from ProductByte, where those screens exist. Sending this with `publish_type` of `scheduled` or `immediate` is refused.
            - `privacy_level` 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY', required: Who can see the post. Required, and must be one the creator currently allows.
            - `allow_comment` boolean, required: Whether viewers may comment.
            - `allow_duet` boolean: Video only.
            - `allow_stitch` boolean: Video only.
            - `auto_add_music` boolean: Photo posts only.
            - `is_aigc` boolean: Discloses AI-generated content.
            - `your_brand` boolean: The post promotes your own brand.
            - `branded_content` boolean: The post promotes a third party. Cannot be combined with `SELF_ONLY`.
        - `pinterest` object: Pinterest settings. Required on a Pinterest channel: every Pin goes on a board.
          - `board_id` string, required: The board the Pin goes on, one of the account’s boards.
          - `title` string: The Pin title. At most 100 characters.
          - `alt_text` string: What the picture shows, for screen readers. At most 500 characters.
    - `campaign_id` string: Group the posts under this campaign, from `/campaigns`. Omit to leave them unassigned — they can be assigned later in ProductByte.
    - `copy` string, required: The text published alongside the media.
    - `replies` object[]: Comments to publish under the post once it is live, in order. Not every channel supports them.
      - `copy` string, required: The comment text. Must not be empty.
      - `delay_seconds` integer: Wait after the previous item publishes, in seconds. At most 30 days.
    - `auto_delete_after_seconds` integer: Remove the post from the channel this long after it publishes. Whole minutes, 60 seconds minimum. Not supported on Instagram or TikTok.
    - `publish_type` 'immediate', required
    - `scheduled_at` string, date-time: Must be omitted. `immediate` publishes now, so a time would be contradictory.

## Response `201`

One post per channel.

- object
  - `posts` object[], required: One post per channel, in the order the channels were given.
    - `id` string, required: Identifier for this post.
    - `channel_id` string, required: The channel it publishes to.
    - `campaign_id` string: The campaign it belongs to. Absent when the post is unassigned — posts still need a home internally, but that grouping is bookkeeping and is never reported as a campaign.
    - `status` 'draft' | 'scheduled' | 'publishing' | 'published' | 'failed' | 'deleted', required: Where the post actually got to. Poll this to learn the outcome.
    - `copy` string, required: The text published alongside the media.
    - `scheduled_at` string, date-time: When it publishes, if it is scheduled.
    - `published_at` string, date-time: When it published, once it has.
    - `permalink` string: Link to the published post, when the provider gives one.
    - `updated_at` string, date-time, required: When the post last changed. Pass the newest one back as `updated_since` to poll for what changed.
    - `replies` object[]: Comments queued under this post, in the order they publish. Absent when there are none.
      - `id` string, required: Identifier for this reply.
      - `copy` string, required: The comment text.
      - `delay_seconds` integer: Wait after the previous item published.
      - `status` 'pending' | 'publishing' | 'published' | 'failed' | 'skipped', required: Where this reply got to. `skipped` means the chain stopped before reaching it.
      - `published_at` string, date-time: When it published, once it has.
    - `settings` object: How this post publishes, as stored. Covers only what this API accepts — a post made in ProductByte may carry more, such as a Reel cover. `link_url` is the effective link: a campaign’s UTM tags are applied to it, so it can differ from the one sent.
      - `link_url` string: Destination URL. X shows it as a call to action on media and a Pin opens it; other channels ignore it.
      - `meta` object: Instagram and Facebook Page settings. A Reel cover and per-image alt text are set in ProductByte rather than here: both are keyed by internal asset ids this API does not expose.
        - `mode` 'feed_post' | 'story_post' | 'reel_post', required: Which Instagram surface to publish to. A Facebook Page takes `feed_post` only. A single video on `feed_post` publishes as a Reel — Instagram has no other container for it.
        - `title` string: Headline for a Facebook Page video. Ignored elsewhere.
        - `share_to_feed` boolean: Reels only. `false` keeps the Reel out of your profile feed, leaving it in the Reels tab.
        - `trial_reel` boolean: Reels only. Publishes to non-followers first. Accounts Instagram finds ineligible publish an ordinary Reel instead, reported back on the post.
        - `ai_generated` boolean: Discloses AI use. Instagram requires it for photorealistic video.
        - `collaborators` string[]: Up to 3 public Instagram usernames, without the leading @, invited to co-author. Instagram does not allow these on a trial reel or a Story.
      - `tiktok` object: TikTok settings.
        - `title` string: Title for the TikTok post. At most 90 characters.
        - `direct_post` object: Publishes straight to TikTok instead of sending the video to the creator’s inbox. Only on a `draft`. TikTok requires the creator to see its consent terms — the Branded Content Policy and Music Usage Confirmation — and to choose from the privacy options their account currently offers. An API call cannot show them either, so a Direct Post is prepared here and published from ProductByte, where those screens exist. Sending this with `publish_type` of `scheduled` or `immediate` is refused.
          - `privacy_level` 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY', required: Who can see the post. Required, and must be one the creator currently allows.
          - `allow_comment` boolean, required: Whether viewers may comment.
          - `allow_duet` boolean: Video only.
          - `allow_stitch` boolean: Video only.
          - `auto_add_music` boolean: Photo posts only.
          - `is_aigc` boolean: Discloses AI-generated content.
          - `your_brand` boolean: The post promotes your own brand.
          - `branded_content` boolean: The post promotes a third party. Cannot be combined with `SELF_ONLY`.
      - `pinterest` object: Pinterest settings. Required on a Pinterest channel: every Pin goes on a board.
        - `board_id` string, required: The board the Pin goes on, one of the account’s boards.
        - `title` string: The Pin title. At most 100 characters.
        - `alt_text` string: What the picture shows, for screen readers. At most 500 characters.

## Other responses

- `400`: The body is invalid, or the schedule time is in the past (`API_REQUEST_INVALID`).
- `401`: Missing, unknown, revoked, or expired key (`API_KEY_UNAUTHORIZED`).
- `403`: The key lacks `posts:write`, the project is outside its allowlist, or the workspace is at its scheduled-post limit.
- `404`: No such creative, format, or channel in this project.
- `409`: The `Idempotency-Key` was already used with a different body (`API_IDEMPOTENCY_KEY_REUSED`), or the first request using it is still running (`API_IDEMPOTENCY_KEY_IN_FLIGHT`) — retry in a moment to collect its response.
- `422`: A chosen format cannot publish to a chosen channel — wrong aspect ratio, or video where only stills are accepted (`API_FORMAT_NOT_PUBLISHABLE`), the channel cannot publish comments (`API_REPLIES_UNSUPPORTED`), or it cannot auto-delete (`API_AUTO_DELETE_UNSUPPORTED`). `details` names what was refused.
- `429`: Too many requests for this key (`API_RATE_LIMITED`). Back off and retry; `Retry-After` gives the window length.

---

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