---
title: "Schedule posts"
method: POST
path: "/api/businesses/{business_id}/posts/schedule"
tags: ["posts"]
---

# Schedule posts

`POST /api/businesses/{business_id}/posts/schedule`

## Request

- Base URL: `https://api.mymarky.ai`
- URL: `https://api.mymarky.ai/api/businesses/{business_id}/posts/schedule`
- Auth: HTTP bearer

Schedule a list of posts in one call: queue them in order, or at local times.

`mode="queue"` (default) appends the posts to the business's posting queue
IN THE ORDER YOU SEND THEM: the first id gets the next open slot. A post
that already carries a date (created with `scheduled_publish_time` while
NEW) is scheduled at that date instead. `mode="at"` publishes each post at
the matching `times_local`, a wall-clock time in the business's timezone;
never convert to UTC yourself.

Each post is answered separately, in the order sent: `publish_time_local`
(quote it verbatim), `warnings` (quote them too), or an `error` for that
post only. One bad post never stops the rest. Calling again with posts that
are already queued changes nothing (ALREADY_SCHEDULED). Every scheduled post
returns a `revision_id` for Undo. `queue` summarizes the queue afterwards,
so no extra read is needed.

Errors (per post, in `results[].error.code`): NOT_FOUND, POST_ALREADY_PUBLISHED,
ALREADY_SCHEDULED, NOT_SCHEDULABLE, CAPTION_HAS_PLACEHOLDER (fill the
[[...]] first), NO_CONNECTED_SOCIALS (follow recovery_hint), MEDIA_REQUIRED,
TIME_IN_PAST.

Errors (whole call):
    401 authentication_required: API key or token is missing, invalid, or revoked.
    403 permission_denied: Your plan does not include API access, or this business is not in your org.
    422 validation_error: No post_ids or more than 50, a malformed id, `times_local` missing or not one per post in `at` mode, or an unknown `timezone`.
    429 rate_limit_exceeded: You sent too many requests this minute.

## Path parameters

- `business_id` string, required: A UUID identifier.

## Headers

- `Marky-Version` '2026-04-08' | '2026-07-17' | '2026-08-02' | '2026-08-04' | '2026-08-06'

## Request body

- SchedulePostsRequest
  - `post_ids` string[], required: The posts to schedule, IN PUBLISH ORDER: in `queue` mode the first id gets the next open slot, the second the one after, and so on. Send the whole batch in one call; parallel single calls can't keep an order.
  - `mode` 'queue' | 'at': `queue` (default): append to the business's posting queue, in list order. A post that already carries a date (a pin) is scheduled at that date instead. `at`: publish each post at the matching `times_local`.
  - `times_local` string[], nullable: `at` mode only, one per post id, in the same order. Wall-clock time in the business's timezone (or `timezone` below), e.g. `2026-10-05T09:00`. Never convert to UTC yourself; daylight saving is handled here.
  - `ignore_copy_date` boolean: `queue` mode: queue posts even when the slot is after the day their caption names (COPY_DATE_BEFORE_NEXT_SLOT). Only when the user said so.
  - `timezone` string, nullable: IANA timezone for `times_local`, e.g. `Australia/Melbourne`. Defaults to the business's calendar timezone. Answers are always shown on the same clock.

## Response `200`

Successful Response

- SchedulePostsResponse
  - `timezone` string, required: IANA timezone of every `*_local` time here.
  - `timezone_source` 'business' | 'unset_utc', required: `business`: the business's own setting. `unset_utc`: none saved, so times are UTC.
  - `results` ScheduledPostResult[], required: One entry per post id, in the order you sent them.
    - `post_id` string, required
    - `ok` boolean, required: True when this call scheduled the post.
    - `placement` 'queue' | 'pin' | 'at' | 'copy_date', nullable: `queue`: in the posting queue. `pin`: at the date it already carried. `at`: at the time you asked for.
    - `position` integer, nullable: Place in the queue (1 = next). Queue placement only.
    - `publish_time` string, nullable: When it goes out (a queue time is an estimate from the posting schedule).
    - `publish_time_local` string, nullable: The same moment in the business's timezone, ready to show a person, e.g. "Wed Sep 23, 6:00 PM MSK". Quote it as-is instead of converting the UTC value.
    - `warnings` FieldWarning[]: Advisory, never blocking; quote them to the user. DATE_BOUND_COPY: the caption says 'today'/a weekday/'this week' and lands on another day. FAR_FUTURE: lands more than 90 days out. SLOT_TAKEN: another post goes to the same account within 30 minutes. PIN_PASSED: its date had passed, so it was queued. QUEUE_PAUSED: the schedule is paused. SCHEDULED_ON_COPY_DATE: the caption names a day the next queue slot was past, so it was scheduled on that day. AFTER_COPY_DATE: it goes out after the day its caption names.
      - `field` string, required: The field this warning is about, e.g. 'strategy', 'caption' or 'platform_overrides[0].caption'.
      - `code` string, nullable: Stable machine-readable code, e.g. 'cta_without_contact'. Branch on this, never on the message. Null on the older size-budget warnings.
      - `level` 'approaching_limit' | 'over_limit' | 'advisory', required: 'approaching_limit' = past the soft limit, act soon. 'over_limit' = past the hard limit, act now. 'advisory' = worth fixing before this publishes, but it may be intended.
      - `message` string, required: Plain-language explanation of what is wrong and how to fix it.
    - `error` ScheduleItemError: Why one post was not scheduled. The rest of the batch is unaffected.
      - `code` 'NOT_FOUND' | 'POST_ALREADY_PUBLISHED' | 'ALREADY_SCHEDULED' | 'NOT_SCHEDULABLE' | 'CAPTION_HAS_PLACEHOLDER' | 'NO_CONNECTED_SOCIALS' | 'MEDIA_REQUIRED' | 'TIME_IN_PAST' | 'COPY_DATE_BEFORE_NEXT_SLOT', required: NOT_FOUND: no such post in this business. POST_ALREADY_PUBLISHED: it already went out. ALREADY_SCHEDULED (queue mode): it is already on the calendar; nothing changed, and its current time is still returned. NOT_SCHEDULABLE: its status can't be scheduled (e.g. REJECTED). CAPTION_HAS_PLACEHOLDER: a [[placeholder]] is still unfilled. NO_CONNECTED_SOCIALS: nothing connected can publish it. MEDIA_REQUIRED: a target platform needs a photo or video. TIME_IN_PAST: `at` time has already passed. COPY_DATE_BEFORE_NEXT_SLOT: the caption names a day, the next queue slot is after it and that day's time has passed; ask the user (publish now, pick a time, or ignore_copy_date=true).
      - `detail` string, required: What happened, in plain words.
      - `recovery_hint` string, nullable: The next step that fixes it, when there is one.
      - `reconnect_url` string, nullable: Where the user reconnects accounts (NO_CONNECTED_SOCIALS).
      - `copy_date` string, date, nullable: The day the caption names (COPY_DATE_BEFORE_NEXT_SLOT).
      - `next_slot_local` string, nullable: Where the queue would have put it, local time (COPY_DATE_BEFORE_NEXT_SLOT).
    - `revision_id` string, nullable: Undo record for this post (revert_revisions).
    - `publish_readiness` PublishReadiness: Where this post will actually go, worked out when it was queued, scheduled or published.
      - `will_publish_to` string[], required: Platforms that will receive the post. Empty means nothing connected can publish it.
      - `blocked` BlockedPlatform[]: Accounts the post was aimed at that won't receive it, each with the reason and fix.
        - `platform` string, required: Platform identifier (facebook, instagram, etc.)
        - `integration_id` string, required: The account that won't receive the post.
        - `reason` string, required: Stable reason code, e.g. INTEGRATION_DELETED (disconnected or needs a reconnect), PAGE_NOT_FOUND (no Page selected), MEDIA_FORMAT_ERROR (this platform needs a photo or video).
        - `message` string, required: Plain-language explanation and fix.
        - `reconnect_url` string, nullable: Where the user reconnects this account, when reconnecting is the fix. Null otherwise.
      - `not_connected` string[]: Platforms the post was aimed at (via restrict_publish_to) that have no account connected. The post will NOT go to these. Connect the account, then schedule or update the post again.
  - `queue` ScheduleQueueSummary, required: The business's queue after this call, so no extra read is needed.
    - `queued_count` integer, required: Posts now waiting in the queue.
    - `runs_dry_at` string, nullable: Estimated time of the LAST queued post.
    - `runs_dry_at_local` string, nullable: The same moment in the business's timezone, ready to show a person, e.g. "Wed Sep 23, 6:00 PM MSK". Quote it as-is instead of converting the UTC value.

## Other responses

- `400`: Bad request — the input was malformed or otherwise invalid.
- `401`: Authentication failed — the API key is missing or invalid.
- `403`: Forbidden — the key is valid but may not access this resource.
- `404`: Not found — no resource matches the id in the path.
- `422`: Validation error — one or more fields failed validation.
- `429`: Rate limited — retry after the window resets (see Retry-After).
- `500`: Server error — something failed on our side; retry or quote X-Request-Id.

---

[API](https://skmtc.dev/mymarky/apis/marky-public-api.md) · [All operations](https://skmtc.dev/mymarky/apis/marky-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mymarky/apis/marky-public-api/revisions/25aeacae455c?raw)
