scheduledAt to a POST /v1/posts body and every target in the batch is persisted with status: "queued". A delayed publish job is enqueued per target at the requested time.
Request
scheduled.sh
Response
You get202 Accepted instead of 200 OK:
202.json
results[].postId per target. To check what happened later, fetch GET /v1/posts/:id for any of them — each returns the full attempt history once its job has run.
Constraints
scheduledAtmust be at least 1 second in the future. Closer than that and you getvalidation_failedwithrule: "scheduledAt.future". The 1-second floor is a guardrail against races where the worker fires before the persisting transaction commits.publishNow: trueis mutually exclusive withscheduledAt. Sending both surfacesvalidation_failedwithrule: "mode_conflict". Drop one of them.- v1 scheduled posts accept text only. Media and
firstCommenton scheduled posts surfacevalidation_failedwithrule: "scheduledAt.text_only". The remediation is to publish synchronously — the scheduled-media path needs persistent media storage and ships in a follow-up slice.
Multi-target scheduling
scheduledAt applies to every target in the batch — same firing time for all of them. The batch is atomic on accept: either every target gets queued, or none do. Each target then runs through its own publish job and surfaces independently in webhooks and the post log.
scheduled-fan-out.sh
Lifecycle
A scheduled target moves through these statuses:post.queued— fired per target immediately on accept.post.published— successful publish on a target.post.rejected— preflight or platform rejected; not retried.post.failed— transient failure; the worker may retry per the queue policy.post.rescheduled— operator moved the firing time viaPATCH.post.canceled— operator canceled the post before it fired.
Reschedule
Move a queued post to a new time withPATCH /v1/posts/:id. The window is status=queued AND scheduledAt > now — once the worker picks the row up, the window closes and the API returns 409.
reschedule.sh
scheduledAt. If the queue swap fails the row stays as-is — no orphaned rows pointing at a job that never fires.
A post.rescheduled webhook fires on success with both the old and new timestamps.
Cancel
Cancel a queued post withDELETE /v1/posts/:id. Same window as reschedule.
cancel.sh
200.json
status: "canceled". A post.canceled webhook fires on success.
Race-safety: if a worker has already started running the job when the cancel lands, the worker’s conditional transition (UPDATE … WHERE status IN ('queued','validated')) sees canceled and short-circuits — the publish never happens.
Idempotency on scheduled posts
Idempotency-Key works the same way as on immediate posts and applies to the whole batch — the replay cache returns the original 202 envelope (same batch id, same per-target rows) if you retry within 24 h with the same body. Each queued job is enqueued exactly once.
Time zones
scheduledAt is ISO-8601 with a timezone offset. Always include the Z (or an explicit +HH:MM); naive datetimes are rejected. We do not interpret a “local” timezone for you.

