---
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-08-20"
---

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.

## 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 }` |

Post sets created via the API never require approval, carry no labels, and are never locked.

### 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`, `tags`, and `products` 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).

### Network-specific fields

| Network | Fields |
| --- | --- |
| `FACEBOOK` | `type` (required): `TEXT`, `IMAGE`, `VIDEO`, `CAROUSEL`, `MULTI_IMAGE`, `REEL`, or `STORY`. `richCaption` (required, see below). Optional `location` (see below), `videoCoverImageSeconds` |
| `INSTAGRAM` | `type` (required): `IMAGE`, `CAROUSEL`, `REEL`, or `STORY`. `collaborators` (required, pass `[]` for none). Optional `location` (see below), `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 |

### Rich captions and mentions

`richCaption` on Facebook and LinkedIn is the caption split into 1 to 32 segments, a mix of plain strings and mention objects, so pages and profiles can be tagged inline. Without mentions, pass the caption as a single-item array: `["Your caption"]`.

```json
[
  "Grabbing a coffee with ",
  { "mention": { "id": "106039436102339", "index": "0", "value": "Pallyy", "denotationChar": "@" } },
  " this morning."
]
```

Each mention object wraps its fields under a `mention` key:

| Field | Meaning |
| --- | --- |
| `id` | The platform's id for the tagged page or profile |
| `index` | Zero-based position of the mention among the caption's mentions, as a numeric string |
| `value` | The display text, without the denotation character |
| `denotationChar` | The character prefixing the mention, usually `@` |

### The location object

Facebook and Instagram posts can tag a place by passing `location`:

```json
{
  "id": "108424279189115",
  "name": "New York, New York",
  "location": {
    "city": "New York",
    "country": "United States",
    "state": "NY",
    "zip": "10001"
  }
}
```

| Field | Required | Meaning |
| --- | --- | --- |
| `id` | Yes | The Facebook place id |
| `name` | Yes | The place's display name |
| `location` | Yes | The place's address, all fields optional: `city`, `country`, `state`, `zip` |

Place ids come from Facebook's places database, the same one Pallyy's composer searches when you tag a location.

### 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`), and repeats (`post-set:repeats_not_available`) 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 |
| `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 | The approval state within Pallyy's approval workflow |
| `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` |
