# Queues and recycling

Give a profile recurring weekly posting slots, add posts to the next free slot with queuedFromProfile, and republish evergreen posts on a schedule.

A queue is a weekly timetable for a profile: Monday 09:00, Wednesday 09:00, Friday 17:30. Instead of picking a time for every post, you add posts to the queue and each one takes the next free slot. Fifty posts become fifty slots without a single `scheduledFor`.

Queues belong to [profiles](https://pozy.ai/docs/build/profiles), not to accounts. A post added to a profile's queue publishes to whichever accounts you list in its `platforms`.

## Create a queue

`POST /v1/queue/slots` takes a profile, a name, a timezone and the weekly slots. `dayOfWeek` runs from `0` (Sunday) to `6` (Saturday) and `time` is `HH:mm` in 24-hour format, read in the queue's timezone.

```bash cURL
curl -X POST https://api.pozy.ai/v1/queue/slots \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "670f9b1d4c2e8a7f3b6d5e10",
    "name": "Weekday mornings",
    "timezone": "Europe/London",
    "slots": [
      { "dayOfWeek": 1, "time": "08:45" },
      { "dayOfWeek": 3, "time": "08:45" },
      { "dayOfWeek": 5, "time": "12:30" }
    ]
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/queue/slots", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    profileId: "670f9b1d4c2e8a7f3b6d5e10",
    name: "Weekday mornings",
    timezone: "Europe/London",
    slots: [
      { dayOfWeek: 1, time: "08:45" },
      { dayOfWeek: 3, time: "08:45" },
      { dayOfWeek: 5, time: "12:30" },
    ],
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

console.log(data.schedule._id, data.nextSlots);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/queue/slots",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "profileId": "670f9b1d4c2e8a7f3b6d5e10",
        "name": "Weekday mornings",
        "timezone": "Europe/London",
        "slots": [
            {"dayOfWeek": 1, "time": "08:45"},
            {"dayOfWeek": 3, "time": "08:45"},
            {"dayOfWeek": 5, "time": "12:30"},
        ],
    },
    timeout=30,
)
queue = resp.json()
print(queue["schedule"]["_id"], queue["nextSlots"])
```

```json
{
  "success": true,
  "schedule": {
    "_id": "6722f1a93b0c4d5e6f7a8b90",
    "profileId": "670f9b1d4c2e8a7f3b6d5e10",
    "name": "Weekday mornings",
    "timezone": "Europe/London",
    "slots": [
      { "dayOfWeek": 1, "time": "08:45" },
      { "dayOfWeek": 3, "time": "08:45" },
      { "dayOfWeek": 5, "time": "12:30" }
    ],
    "active": true,
    "isDefault": true
  },
  "nextSlots": ["2026-11-02T08:45:00+00:00", "2026-11-04T08:45:00+00:00"]
}
```

The first queue a profile gets becomes its default. A profile can have several queues, for example one for product news and one for weekend content. Create another with the same `profileId` and a different `name`.

## Add a post to the queue

Create the post with `queuedFromProfile` set to the profile id and no `scheduledFor`. Pozy locks the next free slot for it, so the next post you add takes the slot after that.

```bash cURL
curl -X POST https://api.pozy.ai/v1/posts \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Tip of the week: export any report as CSV from the share menu.",
    "platforms": [
      { "platform": "linkedin", "accountId": "6710c3e2a9f14b0d8e5c7a21" },
      { "platform": "twitter", "accountId": "6710c3e2a9f14b0d8e5c7a23" }
    ],
    "queuedFromProfile": "670f9b1d4c2e8a7f3b6d5e10"
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    content: "Tip of the week: export any report as CSV from the share menu.",
    platforms: [
      { platform: "linkedin", accountId: "6710c3e2a9f14b0d8e5c7a21" },
      { platform: "twitter", accountId: "6710c3e2a9f14b0d8e5c7a23" },
    ],
    queuedFromProfile: "670f9b1d4c2e8a7f3b6d5e10",
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

console.log(data.post.scheduledFor, data.post.queueId);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/posts",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "content": "Tip of the week: export any report as CSV from the share menu.",
        "platforms": [
            {"platform": "linkedin", "accountId": "6710c3e2a9f14b0d8e5c7a21"},
            {"platform": "twitter", "accountId": "6710c3e2a9f14b0d8e5c7a23"},
        ],
        "queuedFromProfile": "670f9b1d4c2e8a7f3b6d5e10",
    },
    timeout=30,
)
post = resp.json()["post"]
print(post["scheduledFor"], post["queueId"])
```

The post comes back `scheduled`, with the slot's time in `scheduledFor` (UTC) and the queue in `queueId`. Add `queueId` next to `queuedFromProfile` to target a queue other than the default one.

> **Do not copy the next slot into scheduledFor:**
> 
> `GET /v1/queue/next-slot` is for showing people when a post would go out. If you read it and then send that time as `scheduledFor`, two posts created at the same moment can land on the same slot. Send `queuedFromProfile` and let Pozy assign the slot.

## Preview upcoming slots

| Call | Returns |
| --- | --- |
| `GET /v1/queue/next-slot?profileId=...` | The next free slot, its timezone and the queue it belongs to. `404` when the profile has no queue or no free slot. |
| `GET /v1/queue/preview?profileId=...&count=10` | The next `count` slot times (up to 100) as ISO date-times. |
| `GET /v1/queue/slots?profileId=...` | The default queue with its slots and next times. Add `all=true` for every queue of the profile, or `queueId` for one. |

## Change or remove a queue

`PUT /v1/queue/slots` replaces a queue's slots, name, timezone or active state. Without `queueId` it changes the default queue; with `queueId` it changes that one.

| Field | What it does |
| --- | --- |
| `slots`, `timezone` | Required. The full new timetable. |
| `name` | Rename the queue. |
| `active` | `false` pauses the queue. `next-slot` then returns `400` and new posts cannot join it. |
| `setAsDefault` | `true` makes this queue the profile's default. |
| `reshuffleExisting` | `true` moves posts already waiting in the queue onto the new timetable. The response reports `reshuffledCount`. |

`DELETE /v1/queue/slots?profileId=...&queueId=...` deletes one queue. Leave out `queueId` to delete every queue of the profile. Deleting the default queue promotes another one to default.

## Common errors

| Error | Cause | Fix |
| --- | --- | --- |
| `400` on create or update | A slot with `time` not in `HH:mm`, a `dayOfWeek` outside 0 to 6, or an unknown timezone. | Fix the slot or use a canonical IANA name. |
| `404` on `next-slot` | The profile has no queue, or every slot is taken. | Create a queue or add slots. |
| `409` `queue_slot_conflict` on `PUT /v1/posts/{postId}` | You moved a queued post onto a time another post in the same queue already holds. | Pick another time, drop `scheduledFor`, or send `queueId: null` to schedule it outside the queue. |

## Recycle evergreen posts

Recycling republishes a post on a repeating schedule, which suits evergreen content such as guides, FAQs or a pinned offer. Set `recycling` when you create or update the post:

```json
{
  "content": "New here? Our 5-minute setup guide covers everything.",
  "platforms": [{ "platform": "linkedin", "accountId": "6710c3e2a9f14b0d8e5c7a21" }],
  "scheduledFor": "2026-11-02T09:00:00",
  "timezone": "Europe/London",
  "recycling": {
    "enabled": true,
    "gap": 2,
    "gapFreq": "month",
    "expireCount": 6,
    "contentVariations": [
      "New here? Our 5-minute setup guide covers everything.",
      "Setting up a new workspace? Start with the 5-minute guide.",
      "The fastest way to get started is still the 5-minute setup guide."
    ]
  }
}
```

| Field | What it does |
| --- | --- |
| `enabled` | Turns recycling on or off. Defaults to `true` when you send `recycling`. |
| `gap` and `gapFreq` | How long to wait between copies: `gap` weeks or months (`gapFreq` is `week` or `month`, default `month`). |
| `startDate` | When the first copy may go out. |
| `expireCount` | Stop after this many copies. `null` for no limit. |
| `expireDate` | Stop after this date. `null` for no limit. |
| `contentVariations` | Texts to rotate through, so followers do not see the same words every time. |

Each copy is a new post with its own id. It records the original in `recycledFromPostId`, fires `post.recycled`, then follows the normal lifecycle with its own `post.scheduled` and publish events. The original post shows progress in `recycling.recycleCount`, `recycling.nextRecycleAt` and `recycling.lastRecycledAt`.

Recycling is the one thing you can still change on a published post: send a new `recycling` object with `PUT /v1/posts/{postId}`, or `"enabled": false` to stop it.

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/queue/slots`](https://pozy.ai/docs/api/listQueueSlots) | List schedules |
| [`POST /v1/queue/slots`](https://pozy.ai/docs/api/createQueueSlot) | Create schedule |
| [`PUT /v1/queue/slots`](https://pozy.ai/docs/api/updateQueueSlot) | Update schedule |
| [`DELETE /v1/queue/slots`](https://pozy.ai/docs/api/deleteQueueSlot) | Delete schedule |
| [`GET /v1/queue/preview`](https://pozy.ai/docs/api/previewQueue) | Preview upcoming slots |
| [`GET /v1/queue/next-slot`](https://pozy.ai/docs/api/getNextQueueSlot) | Get next available slot |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`post.scheduled`](https://pozy.ai/docs/webhooks/events#post-scheduled) | Post scheduled. |
| [`post.recycled`](https://pozy.ai/docs/webhooks/events#post-recycled) | Post recycled. |
