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

# /posts

`GET /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

Lists the project’s posts, newest first. Filter by `status` to reconcile a batch — for example `published` after scheduling a month of content.

To follow outcomes over time, poll with `updated_since` rather than re-reading the list: it returns only what changed, oldest change first, so a quiet minute costs one empty response. There are no webhooks; this is the intended way to learn that a post published or failed.

Paginate with `cursor`: pass the `next_cursor` from the previous page. It is absent on the last page.

Requires the `posts:read` scope.

## Path parameters

- `project_id` string, required: The project to list posts for.

## Query parameters

- `status` 'draft' | 'scheduled' | 'publishing' | 'published' | 'failed' | 'deleted': Only posts in this state.
- `updated_since` string, date-time: Only posts changed at or after this moment, **oldest change first** — the reverse of the default order. Poll with this instead of re-reading everything: keep the last post’s `updated_at` and pass it back next time. The bound is inclusive, so you will see that post again; deduplicate by `id`.
- `limit` integer: Page size, 1–100.
- `cursor` string: A `next_cursor` from a previous page. Treat it as opaque, and keep the same `status` and `updated_since` when continuing that list.

## Response `200`

A page of posts.

- object
  - `posts` object[], required: Posts, newest first.
    - `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.
  - `next_cursor` string: Pass as `cursor` to fetch the next page. Absent when there are no more.

## Other responses

- `401`: Missing, unknown, revoked, or expired key (`API_KEY_UNAUTHORIZED`).
- `403`: The key lacks `posts:read`, or the project is outside its allowlist.
- `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)
