---
title: "Post sets"
description: "Create, schedule, list, and update Pallyy post sets via the API: one scheduled slot holding a post for each social network it targets."
category: "endpoints"
emoji: "🗓️"
order: 2
datePublished: "2026-08-20"
dateUpdated: "2026-09-04"
---

A post set is one slot on your schedule: a date, a status, and one post per social network it targets. Scheduling the same content to Instagram and X is one post set with two posts, each carrying its own caption, media, and network-specific fields.

A post set moves through four statuses:

| Status | Meaning |
| --- | --- |
| `DRAFT` | Parked, will not publish until scheduled |
| `SCHEDULED` | Will publish at `scheduledAt` |
| `PUBLISHING` | Publishing right now, read-only |
| `PUBLISHED` | Done, read-only. Each post carries a `result` |

Creating or updating a set as `DRAFT` needs the `post-sets:write` scope. Anything that leaves a set `SCHEDULED` additionally needs `post-sets:publish`, see [authentication](/docs/api/authentication).

## List post sets

```
GET /v1/post-sets
```

| Parameter | Required | Meaning |
| --- | --- | --- |
| `socialSetId` | Yes | The social set to list |
| `page`, `size` | No | See [pagination](/docs/api/pagination) |
| `filters[startDate]` | No | ISO 8601, sets scheduled at or after this time |
| `filters[endDate]` | No | ISO 8601, sets scheduled before this time |
| `filters[status][]` | No | One or more of `DRAFT`, `SCHEDULED`, `PUBLISHING`, `PUBLISHED` |
| `filters[socialNetworks][]` | No | Only sets containing posts for these networks, one or more of `INSTAGRAM`, `FACEBOOK`, `TWITTER`, `LINKEDIN`, `GMB`, `PINTEREST`, `TIKTOK`, `YOUTUBE`, `THREADS`, `BLUESKY` |
| `filters[approval][]` | No | One or more of `NONE`, `PENDING`, `EXTERNAL_PENDING`, `APPROVED`, `DENIED` |
| `filters[labelsId][]` | No | Only sets carrying these label ids |
| `filters[search]` | No | Caption text search, at least 3 characters |
| `sort[scheduledAt]`, `sort[createdAt]` | No | `ASC` or `DESC` |

```bash
curl "https://app.pallyy.com/api/v1/post-sets?socialSetId=SET_ID&filters[status][]=SCHEDULED&sort[scheduledAt]=ASC" \
  -H "Authorization: Bearer pallyy_your_api_key"
```

## Get a post set

```
GET /v1/post-sets/{id}
```

Returns the full post set, or a `404` if it does not exist or is outside the key's access.

Any post set can also be opened in Pallyy's calendar with a link carrying its social set in the `set` query parameter and its id in `post`, handy for linking from your own tools back to the app:

```
https://app.pallyy.com/dashboard/scheduling/calendar/month?set=<socialSetId>&post=<postSetId>
```

## Create a post set

```
POST /v1/post-sets
```

Requires `post-sets:write`, plus `post-sets:publish` when `status` is `SCHEDULED`. Returns the created set with a `201`.

| Field | Required | Meaning |
| --- | --- | --- |
| `socialSetId` | Yes | The social set to create in |
| `scheduledAt` | Yes | ISO 8601 date-time the set publishes at |
| `status` | Yes | `DRAFT` or `SCHEDULED` |
| `posts` | Yes | 1 to 11 post objects, one per targeted network |
| `repeat` | No | `{ "interval": "DAY" \| "WEEK" \| "MONTH" \| "YEAR", "occurrences": 2-50 }` |
| `approval` | Yes | The approval state. See below |

### Approvals

`approval` places the set in Pallyy's approval workflow, the same one your team uses in the app:

| Value | Meaning |
| --- | --- |
| `NONE` | No approval needed |
| `PENDING` | Waiting for a team member to approve it in Pallyy |
| `EXTERNAL_PENDING` | Waiting for an external reviewer to approve it via a shared calendar |
| `APPROVED` | Approved |
| `DENIED` | Denied |

The approval state is independent of `status`: it tracks review, while `status` decides whether the set publishes. Any value other than `NONE` requires approvals on your plan; otherwise the request fails with a `403`.

### Repeats

The `repeat` option turns one request into a repeating series. The first occurrence publishes at `scheduledAt`, and each following occurrence is spaced one `interval` apart (`DAY`, `WEEK`, `MONTH`, or `YEAR`), for a total of `occurrences` post sets, between 2 and 50. The series is materialized immediately: one post set per occurrence, all sharing the same `seriesId`, and every occurrence counts against your plan's monthly post limit.

Once a set belongs to a series, its repeat rule can no longer be changed via the API, matching the in-app editor: updating it returns a `400` with `post-set:repeat_not_updatable`. You can still edit each occurrence individually, since each one is a regular post set. Repeats are only available on plans that include them, otherwise the request fails with `post-set:repeats_not_available`.

### Post objects

Every post shares these fields:

| Field | Required | Meaning |
| --- | --- | --- |
| `accountType` | Yes | The network to publish to, see the account `type` values on [social sets](/docs/api/social-sets) |
| `caption` | Yes | The caption text. Each network's own length limit applies |
| `media` | Yes | Media items, may be empty where the network allows text-only posts |

Media items reference your [media library](/docs/api/media) by id, with optional per-item metadata:

```json
{
  "mediaLibraryId": "665f1c2ab7e2a4d1f0a1b2c5",
  "altText": "A latte on a wooden table"
}
```

Besides `altText`, items accept `title`, `description`, `link`, and `tags` (see below) where the network supports them. The referenced media must belong to the same social set as the post set. To get media into the library via the API, see [media uploads](/docs/api/media-uploads).

### Media tags

`tags` places account tags on an image, the way Instagram lets you tag people in a photo. Each tag carries the handle to tag and where on the image it sits:

```json
{
  "mediaLibraryId": "665f1c2ab7e2a4d1f0a1b2c5",
  "tags": [
    { "x": 0.25, "y": 0.4, "text": "pallyy" }
  ]
}
```

| Field | Required | Meaning |
| --- | --- | --- |
| `x` | Yes | Horizontal position, from `0` (left edge) to `1` (right edge) |
| `y` | Yes | Vertical position, from `0` (top edge) to `1` (bottom edge) |
| `text` | Yes | The handle to tag, without the `@` |

Positions are fractions of the image size, so the same tag lands on the same spot regardless of the image's dimensions.

### Network-specific fields

| Network | Fields |
| --- | --- |
| `FACEBOOK` | `type` (required): `TEXT`, `IMAGE`, `VIDEO`, `CAROUSEL`, `MULTI_IMAGE`, `REEL`, or `STORY`. `richCaption` (required, see below). Optional `videoCoverImageSeconds` |
| `INSTAGRAM` | `type` (required): `IMAGE`, `CAROUSEL`, `REEL`, or `STORY`. `collaborators` (required, pass `[]` for none). Optional `firstComment`, `audio`, `audioName`, `videoCoverImage` (seconds into the first video) |
| `LINKEDIN_PAGE`, `LINKEDIN_PERSONAL` | `richCaption` (required, see below) |
| `GMB` | `ctaType` (required): `NONE`, `SIGN_UP`, `CALL`, `SHOP`, `LEARN_MORE`, `ORDER`, or `BOOK`. Optional `ctaUrl` |
| `PINTEREST` | `title` (required), `website` (required), optional `board` as `{ "id", "name" }` |
| `YOUTUBE` | `title` (required), `privacy` (required): `PUBLIC`, `PRIVATE`, or `UNLISTED` |
| `TWITTER`, `TIKTOK`, `THREADS`, `BLUESKY` | Caption and media only |

`richCaption` on Facebook and LinkedIn is the caption as an array of text segments. Pass the caption as a single-item array: `["Your caption"]`.

### Example

```bash
curl https://app.pallyy.com/api/v1/post-sets \
  -X POST \
  -H "Authorization: Bearer pallyy_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "socialSetId": "665f1c2ab7e2a4d1f0a1b2c3",
    "scheduledAt": "2026-09-01T09:30:00.000Z",
    "status": "SCHEDULED",
    "posts": [
      {
        "accountType": "INSTAGRAM",
        "type": "IMAGE",
        "caption": "Fresh from the roastery.",
        "collaborators": [],
        "media": [{ "mediaLibraryId": "665f1c2ab7e2a4d1f0a1b2c5", "altText": "A latte on a wooden table" }]
      },
      {
        "accountType": "TWITTER",
        "caption": "Fresh from the roastery.",
        "media": [{ "mediaLibraryId": "665f1c2ab7e2a4d1f0a1b2c5" }]
      }
    ]
  }'
```

### Validation and plan limits

Drafts are validated loosely, so you can park incomplete content. Scheduling validates every post against its network's rules, and failures return `post-set:invalid_posts` with one entry per problem, see [errors](/docs/api/errors).

Your plan is enforced on every write: the monthly post limit (`post-set:monthly_posts_exceeded`), carousels (`post-set:carousels_not_available`), videos (`post-set:videos_not_available`), repeats (`post-set:repeats_not_available`), and approvals all return a `403` when unavailable.

## Update a post set

```
PATCH /v1/post-sets/{id}
```

Requires `post-sets:write`, plus `post-sets:publish` when the set ends up `SCHEDULED`, whether because you set the status or because it already was. The update is partial: omitted fields keep their saved values.

| Field | Meaning |
| --- | --- |
| `scheduledAt` | New publish time |
| `status` | `DRAFT` or `SCHEDULED` |
| `repeat` | Turn the set into a repeating series. Not allowed on a set that already belongs to one |
| `approval` | New approval state, see [approvals](#approvals). Changing it requires approvals on your plan |
| `posts` | Replaces the set's posts entirely, see below |

When `posts` is provided, it describes the complete new list:

- A post with an `id` keeps that existing post's identity. Its `accountType` cannot change.
- A post without an `id` is created new.
- Existing posts that are not referenced are removed.
- Each post's `media` works the same way: `{ "id": "..." }` keeps an existing media item of the set (it may move between posts), `{ "mediaLibraryId": "..." }` adds a new one from the library, and unreferenced media are removed.
- Each post or media id may be referenced at most once, otherwise the request fails with `post-set:duplicate_references`.

Sets that are `PUBLISHING` or `PUBLISHED` are read-only and return a `412` with `post-set:read_only`.

## The post set object

| Field | Type | Meaning |
| --- | --- | --- |
| `id` | string | The post set id |
| `socialSetId` | string | The social set it belongs to |
| `scheduledAt` | string | ISO 8601 publish time |
| `status` | string | `DRAFT`, `SCHEDULED`, `PUBLISHING`, or `PUBLISHED` |
| `posts` | array | The per-network posts, including their media and network-specific fields |
| `seriesId` | string | Present when the set belongs to a repeating series |
| `repeat` | object | The repeat rule, when the set started a series |
| `labelsId` | array | Label ids applied in Pallyy |
| `approval` | string | `NONE`, `PENDING`, `EXTERNAL_PENDING`, `APPROVED`, or `DENIED`, see [approvals](#approvals) |
| `isLocked` | boolean | Whether the set is locked in Pallyy |
| `createdAt`, `updatedAt` | string | ISO 8601 timestamps |

Once a set has published, each post carries a `result`:

```json
{
  "result": {
    "status": "POSTED",
    "providerId": "18123456789",
    "permalink": "https://www.instagram.com/p/..."
  }
}
```

Failed posts have `"status": "FAILED"` with an optional `message` and one of these `reason` codes:

| Reason | Meaning |
| --- | --- |
| `ACCOUNT_CONNECTION_EXPIRED` | The account's connection expired and needs to be reauthorized in Pallyy |
| `ACCOUNT_RESTRICTED` | The network has restricted the account |
| `ACCOUNT_RESTRICTED_SPAM` | The network has restricted the account for spam |
| `ACCOUNT_SUSPENDED` | The network has suspended the account |
| `ACTION_BLOCKED` | The network blocked the publishing action |
| `CAPTION_TOO_LONG` | The caption exceeds the network's length limit |
| `UNKNOWN_MEDIA_ERROR` | The network rejected the post's media |
| `UNKNOWN` | The publish failed for another reason, check `message` |
