Guides
Scheduling posts
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"
}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=<username> 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=<username>&from=<ISO>&to=<ISO>&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.
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 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.