Post lifecycle

A PostFuze post is a single object that fans out to one target per connected account. The post carries the content, schedule and per-platform configuration; each target tracks the publish to one platform independently. Because targets advance on their own, a post can succeed on some accounts while it retries — or fails permanently — on others. The post-level status you see is a rollup computed from its targets.

Created
draftscheduled
Publishing
publishing
Settled
publishedpartialfailed
A post is created as a draft or scheduled, moves through publishing, and settles into one terminal status — published, partial, or failed.

The two state machines

There are two related lifecycles: the post status (what you poll on GET /posts/{id}) and the target status (one per account, returned in the targets[] array). Understanding both is the key to reading a post correctly.

Post status

StatusMeaning
draftCreated with is_draft: true. Nothing is queued; the post is editable scaffolding only.
scheduledCreated with a future scheduled_at. The post waits until its due time, then starts publishing.
queuedThe default for an immediate post (no scheduled_at, not a draft). Targets start publishing right away.
publishingAt least one target is actively being published (e.g. uploading media chunks). The post is in flight.
publishedEvery target reached the platform successfully.
partialAt least one target published and at least one failed permanently. The post is live on the accounts that succeeded.
failedEvery target failed permanently. Nothing went live.
canceledA draft, scheduled or queued post was deleted before it published.

Target status

Each entry in targets[] moves through its own states. Targets are where retries, backoff and idempotency actually live.

StatusMeaning
pendingBelongs to a draft or scheduled post; not publishing yet.
queuedAccepted and waiting to publish.
publishingActively uploading and posting to the platform (large media uploads happen here).
failedA transient error occurred (e.g. 429, 5xx, network blip). The target is scheduled for a retry with backoff.
publishedLive on the platform. platform_post_id and platform_post_url are populated.
deadFailed permanently — either a non-retryable rejection or retries were exhausted. This is what rolls up into post failure.
canceledThe parent post was deleted before this target published.
Queued
pendingqueued
Publishing
publishingfailed → retry
Terminal
publisheddeadcanceled
Each target runs on its own. A transient error (429 / 5xx / network) sends it back to queued and retries with growing backoff; only dead and canceled are terminal failures.

How status rolls up

Whenever a target reaches a terminal state, PostFuze recomputes the post status from the full set of targets:

  • All targets published → post is published.
  • Some published, some dead → post is partial.
  • All dead → post is failed.
  • Any target still queued/publishing/failed (retrying) → post stays publishing until everything settles.

This is the same partial-success model webhooks use: post.published fires once the post reaches published or partial (at least one account live), and post.error fires only when the post is failed (every account dead). See Webhooks.

Creating a post

A post is created at POST /posts. Provide either a top-level content string or an explicit containers[] array, plus the accounts[] to publish to. Omit scheduled_at and is_draft to publish immediately.

FieldTypeDescription
contentstringText of the main post. Required unless you provide containers.
containersarray (1–10)Ordered { content, media[] } blocks. Position 0 is the main post; see First comments & threads.
accountsstring[] (min 1)Connected social account IDs to fan out to. One target is created per account.
scheduled_atstring (ISO 8601)Future time to publish. Omit to publish now. The post sits in scheduled until due.
is_draftbooleanCreate as a draft with nothing queued. Defaults to false.
configobjectPer-platform configuration, keyed by platform (e.g. { "youtube": { … } }). Applied to each matching target.
external_refstringYour idempotency key. The Idempotency-Key header is accepted as an alias. See Rate limits & idempotency.
curl
curl https://api.postfuze.com/api/v1/posts \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Shipping our unified publishing API today 🚀",
    "accounts": ["acct_x_main", "acct_linkedin_org", "acct_ig_brand"],
    "scheduled_at": "2026-06-10T15:00:00Z"
  }'

The response returns the post with its containers and one target per account. Here the post is scheduled and every target is pending until the due time:

201 Created
{
  "id": "post_8f2a…",
  "status": "scheduled",
  "is_draft": false,
  "scheduled_at": "2026-06-10T15:00:00Z",
  "published_at": null,
  "default_config": {},
  "external_ref": null,
  "created_at": "2026-06-09T12:00:00Z",
  "updated_at": "2026-06-09T12:00:00Z",
  "containers": [
    { "id": "ctr_…", "position": 0, "role": "main", "content": "Shipping our unified publishing API today 🚀" }
  ],
  "targets": [
    { "id": "tgt_x", "social_account_id": "acct_x_main", "platform": "x", "status": "pending", "platform_post_id": null, "platform_post_url": null, "error_code": null, "error_message": null, "published_at": null },
    { "id": "tgt_li", "social_account_id": "acct_linkedin_org", "platform": "linkedin", "status": "pending", "platform_post_id": null, "platform_post_url": null, "error_code": null, "error_message": null, "published_at": null },
    { "id": "tgt_ig", "social_account_id": "acct_ig_brand", "platform": "instagram", "status": "pending", "platform_post_id": null, "platform_post_url": null, "error_code": null, "error_message": null, "published_at": null }
  ]
}

Reading a post

Poll GET /posts/{id} to watch the lifecycle advance. Below, the post has settled into partial: X and LinkedIn are live, while Instagram hit a permanent rejection and is dead.

GET /posts/post_8f2a…
{
  "id": "post_8f2a…",
  "status": "partial",
  "is_draft": false,
  "scheduled_at": "2026-06-10T15:00:00Z",
  "published_at": "2026-06-10T15:00:04Z",
  "containers": [
    { "id": "ctr_…", "position": 0, "role": "main", "content": "Shipping our unified publishing API today 🚀" }
  ],
  "targets": [
    { "id": "tgt_x", "social_account_id": "acct_x_main", "platform": "x", "status": "published", "platform_post_id": "1799…", "platform_post_url": "https://x.com/…/status/1799…", "error_code": null, "error_message": null, "published_at": "2026-06-10T15:00:02Z" },
    { "id": "tgt_li", "social_account_id": "acct_linkedin_org", "platform": "linkedin", "status": "published", "platform_post_id": "urn:li:share:7203…", "platform_post_url": "https://www.linkedin.com/feed/update/urn:li:share:7203…", "error_code": null, "error_message": null, "published_at": "2026-06-10T15:00:04Z" },
    { "id": "tgt_ig", "social_account_id": "acct_ig_brand", "platform": "instagram", "status": "dead", "platform_post_id": null, "platform_post_url": null, "error_code": "rejected_400", "error_message": "Media aspect ratio not supported", "published_at": null }
  ]
}

Per-account targets

Each target carries everything you need to inspect one account's outcome:

  • social_account_id / platform — which connected account and network this target publishes to.
  • status — the target state machine above.
  • platform_post_id / platform_post_url — the native post ID and permalink, populated on success.
  • error_code / error_message — the classified failure (e.g. rejected_400, rate_limited, token_expired) when a target is failed or dead.
  • published_at — when this specific account went live.

Per-platform config is applied to the target whose platform matches the key, so the same post can carry a YouTube title and an Instagram story flag at once without conflicting.

Canceling

DELETE /posts/{id} soft-deletes the post. If it is still draft, scheduled or queued, the post (and its un-published targets) move to canceled and never publish. Content that has already gone live on a platform stays there — PostFuze does not delete published posts upstream.

curl
curl -X DELETE https://api.postfuze.com/api/v1/posts/post_8f2a… \
  -H "Authorization: Bearer sk_live_…"
200 OK
{ "id": "post_8f2a…", "canceled": true }