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.
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
| Status | Meaning |
|---|---|
draft | Created with is_draft: true. Nothing is queued; the post is editable scaffolding only. |
scheduled | Created with a future scheduled_at. The post waits until its due time, then starts publishing. |
queued | The default for an immediate post (no scheduled_at, not a draft). Targets start publishing right away. |
publishing | At least one target is actively being published (e.g. uploading media chunks). The post is in flight. |
published | Every target reached the platform successfully. |
partial | At least one target published and at least one failed permanently. The post is live on the accounts that succeeded. |
failed | Every target failed permanently. Nothing went live. |
canceled | A 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.
| Status | Meaning |
|---|---|
pending | Belongs to a draft or scheduled post; not publishing yet. |
queued | Accepted and waiting to publish. |
publishing | Actively uploading and posting to the platform (large media uploads happen here). |
failed | A transient error occurred (e.g. 429, 5xx, network blip). The target is scheduled for a retry with backoff. |
published | Live on the platform. platform_post_id and platform_post_url are populated. |
dead | Failed permanently — either a non-retryable rejection or retries were exhausted. This is what rolls up into post failure. |
canceled | The parent post was deleted before this target published. |
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 ispublished. - Some
published, somedead→ post ispartial. - All
dead→ post isfailed. - Any target still
queued/publishing/failed(retrying) → post stayspublishinguntil 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.
| Field | Type | Description |
|---|---|---|
content | string | Text of the main post. Required unless you provide containers. |
containers | array (1–10) | Ordered { content, media[] } blocks. Position 0 is the main post; see First comments & threads. |
accounts | string[] (min 1) | Connected social account IDs to fan out to. One target is created per account. |
scheduled_at | string (ISO 8601) | Future time to publish. Omit to publish now. The post sits in scheduled until due. |
is_draft | boolean | Create as a draft with nothing queued. Defaults to false. |
config | object | Per-platform configuration, keyed by platform (e.g. { "youtube": { … } }). Applied to each matching target. |
external_ref | string | Your idempotency key. The Idempotency-Key header is accepted as an alias. See Rate limits & idempotency. |
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:
{
"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.
{
"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 isfailedordead.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 -X DELETE https://api.postfuze.com/api/v1/posts/post_8f2a… \
-H "Authorization: Bearer sk_live_…"{ "id": "post_8f2a…", "canceled": true }