# Scheduling Posts · PostLake Docs > Markdown version of https://docs.postlake.dev/scheduling . The canonical page for humans. > PostLake is the social media API for AI agents: https://postlake.dev/llms.txt Add a `scheduledAt` and the post is queued and fires later. Credits are charged when it publishes. Not when you schedule it. Invalid media (an oversized image, a dead connection) is refused at create time, not at 7am. A TikTok PNG is converted to JPEG at create, with a warning. ## Schedule a post Send an ISO-8601 timestamp. UTC with a trailing `Z` still works. For a wall-clock time in a named zone, pass `timezone` (IANA) with a naive `scheduledAt`. The stored fire time is always UTC. The post comes back with `state: "scheduled"`. ``` { "text": "Friday recap", "accounts": ["acc_123"], "scheduledAt": "2026-08-13T07:00:00", "timezone": "Europe/London" } ``` That queues for 07:00 BST (06:00 UTC). The response includes `scheduledAt` in UTC and `scheduledAtLocal` in the zone you named. Existing posts that were stored as UTC are unchanged. A naive datetime without `timezone` is a 400 unless the account has a default timezone (`PATCH /v1/me` with `{ "timezone": "Europe/London" }`). "7am" with a trailing `Z` is 7am UTC, which is 8am in British Summer Time. If the account has a default timezone, a trailing-Z time is still that UTC instant; the zone is stored only so lists can show local time. ## Timezones Prefer `timezone` plus a naive local time when a person said "7am UK". Convert yourself only when you already have a UTC instant. Set an account default once if every post is in the same zone: ``` PATCH /v1/me { "timezone": "Europe/London" } ``` Then a naive `scheduledAt` of `2026-08-13T07:00:00` is 07:00 London without repeating the zone on every call. ``` { "scheduledAt": "2026-08-13T07:00:00", "timezone": "Europe/London" } ``` Sending a local time as UTC (trailing `Z`, no `timezone`) is still the #1 scheduling bug. Posts fire an hour off. The dashboard composer converts for you. ## Edit or cancel before it fires While a post is `scheduled` you can change it or pull it entirely, by id: | Action | Endpoint | | --- | --- | | Edit text, media, destinations, options, or time | `PATCH /v1/posts/{id}` | | Cancel only a scheduled post | `POST /v1/posts/{id}/cancel` | `PATCH` can change `text`, `media`, `mediaAlt`, `mediaOverrides`, `platformOptions`, `scheduledAt`, `timezone`, and `accounts`. Pass the complete final list of connected `acc_…` IDs in `accounts` to add or remove destinations. Unchanged destinations are preserved. New destinations use the same scheduled time and must pass content validation. Destination edits close 30 seconds before publishing, or as soon as publishing starts. An approval-required agent cannot change an approved post's destinations; the owner can review and make that change in the dashboard. A Profile Group is a selection aid: changing its membership does not alter an existing scheduled post. `platformOptions` merge per platform, so `{ "tiktok": { "title": "…" } }` does not wipe other TikTok fields. Cancellation retains the original `Idempotency-Key` receipt. Replaying the same key does not create a replacement. Use a new key only for an intentional new schedule. When changing `accounts` on a scheduled post, also send `expectedAccounts`: the post's current target account IDs from `GET /v1/posts/{id}` in order. This refuses stale edits instead of overwriting someone else's changes. `GET /v1/posts` (and MCP `list_posts`) accept `?account=acc_…` and `?profile=` to show one channel or one profile. Unknown values are 404. Scheduled times in list views always include UTC; when a zone is known they read like `2026-08-14 07:00 Europe/London (06:00 UTC)`. ## Managing a large calendar Bulk-edit preview snapshots cover the full stored posts, including media and platform options edited through single-post PATCH, not just the fields shown in the before/after diff. Any intervening stored change requires another preview. Give related posts the same `campaign` slug when creating them, for example `founder-drop-01`. Optional `assetMetadata` can record short facts such as hook, music track, footage type and source filename on each post. Query `GET /v1/posts?campaign=founder-drop-01&state=scheduled&sort=scheduledAt` to find the campaign. Add `targetCount=single` or `targetCount=shared` to distinguish standalone posts from shared multi-channel schedules. To change up to 200 scheduled posts together, send their exact IDs to `PATCH /v1/posts/bulk` with `dryRun: true`. Inspect the per-post before and after values, then send the same request with `dryRun: false` and the returned `snapshot`. Storage changes all together or not at all. `accounts` replaces the exact destination list on every selected post. `shiftMinutes` moves each UTC instant by elapsed minutes, not local wall-clock time across daylight-saving changes. Timers are re-armed best-effort after storage changes, with the due-post cron as a backstop. To create 1 to 200 scheduled posts in one transaction, send their post bodies to `POST /v1/posts/calendar`. Give each a future `scheduledAt`, or add a `cadence` with `startAt` and `everyMinutes` to assign elapsed UTC slots in order. Preview with `dryRun: true`, inspect times and destinations, then apply with the returned `snapshot` and a unique `idempotencyKey`. The posts and their receipt are stored together or not at all. A retry with the same key returns the original posts. Bulk cancellation through `POST /v1/posts/bulk/cancel` also removes all selected scheduled rows in one transaction. If a post changes or starts publishing, none are cancelled. This is not an atomic swap with a replacement calendar: create and verify the new calendar before cancelling the old one. To plan a regular local-time cadence, call `POST /v1/posts/cadence/preview` or MCP `preview_cadence` with an IANA timezone, local start date and time, day count, and named tracks with optional stagger minutes. It returns exact UTC slots without creating posts, reserving slots, or charging credits. Supply `occupiedAt` from your existing calendar to avoid those times. The planner rejects nonexistent and repeated daylight-saving local times rather than guessing. Feed its UTC slots into the calendar batch as explicit `scheduledAt` values. Platform pacing and credits remain authoritative when posts publish. Use `GET /v1/posts?state=scheduled&sort=scheduledAt&profile=&from=&to=&limit=100` to read the calendar in scheduled-time order. Keep passing `nextCursor` until it is null. This cursor continues past a cancelled post on an earlier page; the default creation-time cursor does not have that guarantee. Supply explicit timezone-qualified timestamps for `from` and `to`. For a planned batch cancellation, first collect the exact post IDs from the calendar. Then preview it: ``` POST /v1/posts/bulk/cancel { "ids": ["post_abc", "post_def"], "profile": "my-brand", "from": "2026-10-01T00:00:00Z", "to": "2026-11-01T00:00:00Z", "dryRun": true } ``` Send the same request with `dryRun: false` and the returned `snapshot` to cancel. The snapshot covers every stored field, including media and platform options, plus the selection scope. Up to 200 unique IDs are accepted. All posts must remain scheduled in the requested profile and time window. Any change or publishing race produces HTTP 409 with no cancellation. Success is HTTP 200, never a partial HTTP 207. This is atomic cancellation, not an atomic swap with a replacement calendar. Legacy requests without a snapshot retain execution-time protection only. ### Cancel one post without risking retraction Use `POST /v1/posts/{id}/cancel` with `{ "dryRun": true }`, then apply with `{ "snapshot": "RETURNED_SNAPSHOT" }`. It only cancels scheduled posts and never retracts published content. The SDK method is `posts.cancelScheduled`; MCP `cancel_post` supports the same preview and snapshot, and accepts scoped `ids` for batch cancellation. Generic `DELETE /v1/posts/{id}` can retract published content; do not use it for cancel-only integrations. For regular cadence, generate each timestamp in the intended IANA time zone, validate each post, then create it with a distinct `Idempotency-Key`. Reuse the same key only when retrying that same create request. Space scheduled times according to the connected platform's limits, and review the returned `scheduledAt` values before handing the calendar to someone else. `POST /v1/posts/validate` returns `{ "ok": boolean, "targets": [...] }`. Each target has `account`, `platform`, `ok`, `errors`, `warnings`, and `issues` (`code`, `message`, `fix`, optional `param`, and `docs`). Warnings do not block a target. A malformed request may return HTTP 400 instead of a validation result. Validation does not reserve a future publishing slot or guarantee that a platform will accept a post later. ## Schedules with human approval An approval-required submission stays `awaiting_approval`, even if it has a requested publish time. It is not armed and cannot fire before a human approves it. Approval preserves the ID and metadata. Approve and schedule keeps the requested future time; approve and publish explicitly clears it. A past requested time must be replaced with a future time or cleared to publish now. Pending content can be edited and rejected without publishing. After approval, a scheduled post uses the normal scheduled edit and scheduled-only cancel endpoints. Preview cancellation, then send its full-state snapshot on apply. If publishing starts first, cancellation returns 409 and never retracts the publication. Bulk cancellation is all-or-none; generic multi-target deletion/retraction can return partial 207 results and is a different operation. The uploaded-media policy applies on every plan while the account remains open, including approval-required, free and PAYG integrations. `GET /v1/media/policy` is the machine-readable contract: uploaded assets have a 90-day minimum retention horizon, with each scheduled reference protected through terminal outcome plus 30 days if later. There is no automatic expiry currently. Cancellation, channel disconnection and subscription cancellation do not delete the uploaded asset. Source URLs are public, including for private destination posts. Keep a durable source yourself for disaster recovery. External URLs are controlled by their host and are not covered by uploaded-media retention. See the [retention terms and exceptions](https://postlake.dev/terms#media-retention). ## Disconnecting a scheduled destination On Channels, choose Disconnect to preview the affected schedules. Nothing changes until you confirm one of these choices: - **Remove from scheduled posts:** only this destination is removed. Other destinations keep their schedules. If no destinations remain, the post becomes a draft with its content and media preserved. - **Keep for reconnection:** scheduled targets retain the account identity. This destination cannot publish until the same account is reconnected. Other valid destinations can still publish, and a missing destination is not charged. Published posts stay untouched. PostLake never guesses why an account was disconnected or substitutes another account. For already disconnected targets, open the scheduled post to remove the destination or reconnect the intended account. API integrations read `GET /v1/social-accounts/{id}/disconnect-preview`, ask the owner to choose, then call `DELETE /v1/social-accounts/{id}?scheduledPosts=remove&expectedSnapshot=...`. Use the returned `snapshot`. The compatibility default is `keep`; passing its snapshot is recommended. MCP uses `disconnect_account` with `preview: true`, then `scheduled_posts` and `expected_snapshot`. Preview runs only on demand. Confirmation checks full post state and publishing claims in one transaction. Posts already publishing or due within 30 seconds block the action. At most 100 affected schedules and 1.5 MB of confirmation content are processed per disconnect. For larger selections, use bulk destination edits in smaller batches first. If confirmation fails, inspect the current channel and schedules before trying again; do not blindly replay a disconnect. ## What happens at fire time At the scheduled moment the post moves from `scheduled` to `queued` while a worker claims the job. It then becomes `published`, `processing` while the network confirms an asynchronous upload, `partial`, or `failed`. Credits are reserved immediately before sending each destination. Confirmed failures are refunded. Subscribe to [webhooks](https://postlake.dev/api#res-webhooks) for outcome notifications. ## Interrupted publishing and recovery A queued job has a ten-minute publishing lease. A fast delivery retry waits for that lease instead of abandoning the post. A bounded background check also discovers expired scheduled claims. Recovery atomically claims the post again only when no prior publishing charge or provider receipt makes a resend unsafe. Claim checks also fence late workers from charging or overwriting a recovered job. If a prior charge exists but the provider receipt was not saved, the outcome is uncertain. PostLake does not automatically publish another copy or assume a refund is due. Read the original post with `GET /v1/posts/{id}` and contact support to reconcile the provider outcome. Do not repeat creation to recover an uncertain publication. Overdue jobs appear in dashboard attention notices, Post Detail and Admin Health.