# Scheduler API

Publish posts later: set a time with scheduledFor, or give a profile weekly queue slots that new posts fill in order, with no worker on your side.

import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

The Scheduler API publishes posts at a time you choose. Give a post a `scheduledFor`, or let a profile's queue pick the next free slot. Zernio publishes it, with no worker on your side.

## What you can build

- **A content calendar.** Create posts with `scheduledFor` and `timezone` through `POST /v1/posts`, then move them with `PUT /v1/posts/{postId}`.
- **A "post at the best time" queue.** Create weekly slots with `POST /v1/queue/slots` and add posts with `queuedFromProfile`. Each takes the next free slot.
- **Approval before publishing.** Schedule with `requiresApproval: true`. The post waits until `POST /v1/posts/{postId}/review` approves it.
- **Bulk imports.** Schedule many posts from one CSV file with `POST /v1/posts/bulk-upload`.
- **Evergreen reposting.** Add a `recycling` rule to a post so it publishes again on a weekly or monthly gap. YouTube and TikTok are excluded.

## Platforms

<ProductPlatforms product="scheduler" />

Scheduling works on every platform the [Posting API](/products/posting) publishes to, with the same body. Only the publish time changes.

Platform rules apply at publish time. A Reddit or Snapchat entry fails then, for the reasons the Posting API lists.

## How it works

A scheduled post is a normal post with `status: "scheduled"` and a `scheduledFor` time. You get its `_id` back at once and use it to read, move or delete the post.

- **A fixed time.** `scheduledFor` is ISO 8601. A value with `Z` or an offset is taken as is. Without one, it is local time in `timezone`, so 9:00 stays 9:00 across daylight saving ([timezones](/guides/timezones)).
- **A queue.** A queue is a set of weekly slots on a profile, each a `dayOfWeek` (0 is Sunday) and a `time`. A profile can have several queues; the first one is the default.
- **Per-platform times.** A `scheduledFor` on one `platforms` entry overrides the shared time for that platform.

At its time the post moves to `publishing`, then `published`, `partial` or `failed`. [Post webhooks](/webhooks/social) tell you how each platform went.

## Your first request

You need an API key and the `accountId` of a connected account ([connecting accounts](/guides/connecting-accounts)). This schedules one post for 9:00 in New York.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data } = await zernio.posts.createPost({
  body: {
    content: 'Our spring sale starts today.',
    scheduledFor: '2027-03-01T09:00:00',
    timezone: 'America/New_York',
    platforms: [
      { platform: 'linkedin', accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
    ]
  }
});

console.log(data.post._id, data.post.scheduledFor);
```
</Tab>
<Tab value="Python">
```python
from zernio import Zernio

client = Zernio()

scheduled = client.posts.create_post(
    content="Our spring sale starts today.",
    scheduled_for="2027-03-01T09:00:00",
    timezone="America/New_York",
    platforms=[
        {"platform": "linkedin", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"}
    ],
)

print(scheduled["post"]["_id"], scheduled["post"]["scheduledFor"])
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/posts" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Our spring sale starts today.",
    "scheduledFor": "2027-03-01T09:00:00",
    "timezone": "America/New_York",
    "platforms": [
      { "platform": "linkedin", "accountId": "66b2e19d8c3f5a7e9d0b1c2d" }
    ]
  }'
```
</Tab>
</Tabs>

Response (`201`):

```json
{
  "message": "Post scheduled successfully",
  "post": {
    "_id": "65f1c0a9e2b5af0012ab34cd",
    "content": "Our spring sale starts today.",
    "status": "scheduled",
    "scheduledFor": "2027-03-01T14:00:00Z",
    "timezone": "America/New_York",
    "platforms": [
      { "platform": "linkedin", "status": "pending" }
    ]
  }
}
```

`scheduledFor` comes back in UTC. The post stays `scheduled` until then.

## Examples

### Fill a weekly queue

Create a queue with three slots a week, then add posts with `queuedFromProfile` and no `scheduledFor`. Each post takes the next free slot ([queue scheduling](/guides/queue-scheduling)).

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
const profileId = '66a1f0c2a4b9d3e8f1a2b3c4';

await zernio.queue.createQueueSlot({
  body: {
    profileId,
    name: 'Weekday mornings',
    timezone: 'America/New_York',
    slots: [
      { dayOfWeek: 1, time: '09:00' },
      { dayOfWeek: 3, time: '09:00' },
      { dayOfWeek: 5, time: '09:00' }
    ]
  }
});

const { data: queued } = await zernio.posts.createPost({
  body: {
    content: 'Tip of the week: cache your dependencies.',
    platforms: [{ platform: 'linkedin', accountId: '66b2e19d8c3f5a7e9d0b1c2d' }],
    queuedFromProfile: profileId
  }
});

console.log(queued.post.scheduledFor);
```
</Tab>
<Tab value="Python">
```python
profile_id = "66a1f0c2a4b9d3e8f1a2b3c4"

client.queue.create_queue_slot(
    profile_id=profile_id,
    name="Weekday mornings",
    timezone="America/New_York",
    slots=[
        {"dayOfWeek": 1, "time": "09:00"},
        {"dayOfWeek": 3, "time": "09:00"},
        {"dayOfWeek": 5, "time": "09:00"},
    ],
)

queued = client.posts.create_post(
    content="Tip of the week: cache your dependencies.",
    platforms=[{"platform": "linkedin", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"}],
    queued_from_profile=profile_id,
)

print(queued["post"]["scheduledFor"])
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/queue/slots" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "66a1f0c2a4b9d3e8f1a2b3c4",
    "name": "Weekday mornings",
    "timezone": "America/New_York",
    "slots": [
      { "dayOfWeek": 1, "time": "09:00" },
      { "dayOfWeek": 3, "time": "09:00" },
      { "dayOfWeek": 5, "time": "09:00" }
    ]
  }'

curl -X POST "https://zernio.com/api/v1/posts" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Tip of the week: cache your dependencies.",
    "platforms": [{ "platform": "linkedin", "accountId": "66b2e19d8c3f5a7e9d0b1c2d" }],
    "queuedFromProfile": "66a1f0c2a4b9d3e8f1a2b3c4"
  }'
```
</Tab>
</Tabs>

To show a user when the next post would go out, read `GET /v1/queue/next-slot?profileId=...`. Do not copy that time into `scheduledFor`: send `queuedFromProfile`, which locks the slot.

### Move a scheduled post

Send the new time to `PUT /v1/posts/{postId}`. Draft, scheduled, failed, partial and cancelled posts can be edited.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
const { data } = await zernio.posts.updatePost({
  path: { postId: '65f1c0a9e2b5af0012ab34cd' },
  body: {
    scheduledFor: '2027-03-02T09:00:00',
    timezone: 'America/New_York'
  }
});
```
</Tab>
<Tab value="Python">
```python
updated = client.posts.update_post(
    "65f1c0a9e2b5af0012ab34cd",
    scheduled_for="2027-03-02T09:00:00",
    timezone="America/New_York",
)
```
</Tab>
<Tab value="curl">
```bash
curl -X PUT "https://zernio.com/api/v1/posts/65f1c0a9e2b5af0012ab34cd" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "scheduledFor": "2027-03-02T09:00:00", "timezone": "America/New_York" }'
```
</Tab>
</Tabs>

### Cancel a scheduled post

`DELETE /v1/posts/{postId}` removes a post that has not published, and refunds its upload quota. A published post cannot be deleted; unpublish it instead ([unpublish post](/posts/unpublish-post)).

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
await zernio.posts.deletePost({ path: { postId: '65f1c0a9e2b5af0012ab34cd' } });
```
</Tab>
<Tab value="Python">
```python
client.posts.delete_post("65f1c0a9e2b5af0012ab34cd")
```
</Tab>
<Tab value="curl">
```bash
curl -X DELETE "https://zernio.com/api/v1/posts/65f1c0a9e2b5af0012ab34cd" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"
```
</Tab>
</Tabs>

## How it behaves

- **A time in the past publishes now.** The post publishes during the request, like `publishNow`.
- **No `timezone` means the profile's.** A post without one takes its profile's timezone, or UTC when there is none.
- **Platforms publish independently.** One that fails leaves the others published, and the post becomes `partial`.
- **Moving a queued post keeps it in its queue.** A new time on a taken slot returns `409`. Send `queueId: null` to schedule it outside the queue.
- **A draft needs `isDraft: false` to schedule.** Sending only `scheduledFor` to a draft leaves it a draft.

## If it fails

| Error | Cause | Fix |
|---|---|---|
| `400` on `POST /v1/posts` | `scheduledFor` is not ISO 8601, `timezone` is unknown, or `platforms` is empty | Fix the field the `error` names |
| `400` on `DELETE /v1/posts/{postId}` | The post is already published | Unpublish it instead |
| `409` on `PUT /v1/posts/{postId}` | The new time is taken in the same queue (`queue_slot_conflict`) | Pick a free time, or omit `scheduledFor` to take the next slot |
| `404` on `GET /v1/queue/next-slot` | The profile has no queue, or no free slot | Create a queue or add slots |

## Related

- [Queue scheduling](/guides/queue-scheduling)
- [Timezones](/guides/timezones)
- [Post lifecycle](/guides/post-lifecycle)
- [Queue reference](/queue/list-queue-slots)
- [Posting API](/products/posting)