Posts
A post is a single publish intent that fans out to one or more connected accounts. Creating a post produces one target per account, each with its own independent status — so a post can publish on some platforms while it retries on others. The same post body can be plain content or a structured containers array that models a main post, a first comment, and a thread.
Posts are idempotent: supply an external_ref or an Idempotency-Key header and a replayed request returns the original post with a 200 instead of creating a duplicate.
Containers, comments, and threads
Instead of a flat content string, you can pass containers — an ordered array (1–10 items) where each entry has its own content and media. The position of each container determines its role:
- Position 0 →
main— the root post. - Position 1 →
first_comment— published as a reply to the root. - Position 2+ →
thread— chained replies forming a thread.
If you send a top-level content field instead, it is normalized into a single main container. See the Replies page for how first comments and threads are published per platform.
Per-platform config
Per-platform settings are keyed by platform identifier. When a post fans out, each target receives the config slice for its own platform (for example the youtube block for a YouTube target). This is how you set things like a YouTube title, a Pinterest board, or a TikTok privacy level.
You can supply these either way — both are accepted:
- nested under a
configobject:{ "config": { "pinterest": { "board_id": "…" } } }, or - as top-level platform keys on the post body:
{ "pinterest": { "board_id": "…" }, "tiktok": { "privacy_level": "…" } }.
When a platform appears in both forms, the nested config value wins. The resolved map is echoed back on the response as default_config.
Create a post
POST /posts
Creates a post and fans it out to every account in accounts[]. Immediate (non-draft, unscheduled) posts start publishing right away; scheduled posts publish when due; drafts stay idle until updated.
| Parameter | Type | Required | Description |
|---|---|---|---|
content | string | Conditional | Text of a single-container post. Provide either content or containers. |
containers | object[] | Conditional | Ordered containers (1–10). Each has content (string) and media (array of { id, alt_text? }). |
accounts | string[] | Yes | Target accounts, referenced by id (UUID), username, or network_unique_id. At least one is required; a handle that resolves to more than one account is rejected with 422. |
scheduled_at | string (ISO 8601) | No | When to publish. Omit to publish immediately. |
queued_from_account | string | No | Assign the post to the next open recurring posting slot for one of the accounts in accounts[]. Mutually exclusive with scheduled_at and drafts. |
queue_timezone | string (IANA timezone) | No | Wall-clock timezone used to resolve queued_from_account slots. Defaults to UTC. |
is_draft | boolean | No | Create the post without queuing it. Defaults to false. |
config | object | No | Per-platform configuration, keyed by platform identifier. May also be passed as top-level platform keys (see above). |
external_ref | string | No | Idempotency key (max 255). Falls back to the Idempotency-Key header. |
Each media item references a previously uploaded media asset by id and may include alt_text. Referenced media must be in a ready or uploaded state, and every account must belong to your organization, or the request fails with 422.
Use queued_from_account when you want PostFuze to drip-feed posts through your recurring posting slots. Unlike calling /scheduling/find-slot and then sending scheduled_at, queue mode reserves the next open slot as part of creation so concurrent posts do not pick the same time.
curl https://api.postfuze.com/api/v1/posts \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: launch-2026-06-09" \
-d '{
"containers": [
{
"content": "Launch day is here 🚀 Read the announcement:",
"media": [{ "id": "m_9f8e7d6c", "alt_text": "Product hero image" }]
},
{ "content": "Full write-up: https://acme.com/blog/launch", "media": [] }
],
"accounts": ["a1b2c3d4-…", "b7c8d9e0-…"],
"scheduled_at": "2026-06-10T15:00:00.000Z",
"config": {
"youtube": { "title": "Acme Launch", "privacyStatus": "public", "isShort": false }
}
}'{
"id": "p1a2b3c4-0000-4a9f-9b1c-2e7d6f0a1234",
"status": "scheduled",
"is_draft": false,
"scheduled_at": "2026-06-10T15:00:00.000Z",
"published_at": null,
"default_config": {
"youtube": { "title": "Acme Launch", "privacyStatus": "public", "isShort": false }
},
"external_ref": "launch-2026-06-09",
"created_at": "2026-06-09T12:20:00.000Z",
"updated_at": "2026-06-09T12:20:00.000Z",
"containers": [
{ "id": "c_0001", "position": 0, "role": "main", "content": "Launch day is here 🚀 Read the announcement:" },
{ "id": "c_0002", "position": 1, "role": "first_comment", "content": "Full write-up: https://acme.com/blog/launch" }
],
"targets": [
{
"id": "t_0001",
"social_account_id": "a1b2c3d4-…",
"platform": "x",
"status": "pending",
"platform_post_id": null,
"platform_post_url": null,
"error_code": null,
"error_message": null,
"published_at": null
},
{
"id": "t_0002",
"social_account_id": "b7c8d9e0-…",
"platform": "youtube",
"status": "pending",
"platform_post_id": null,
"platform_post_url": null,
"error_code": null,
"error_message": null,
"published_at": null
}
]
}A replayed request carrying an already-seen external_ref returns the original post with status 200 OK rather than 201.
curl https://api.postfuze.com/api/v1/posts \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"content": "Queued post content",
"accounts": ["a1b2c3d4-…"],
"queued_from_account": "a1b2c3d4-…",
"queue_timezone": "America/New_York"
}'Create posts in a batch
POST /posts/batch
Creates up to 20 posts in one request. The body is { "posts": [ … ] } where each element is exactly the single-post create body documented above. Each item is created independently — one bad item (a gate failure, missing media, or an unknown account) is reported in errors and never aborts the rest. The endpoint always responds 207 Multi-Status with a data array (created posts, each with its own index and status) and an errors array (failed items, each with its index, HTTP status, and title/detail).
The request-level Idempotency-Key header is ignored for batch (it would collapse every item onto one post) — set a per-item external_ref instead.
curl https://api.postfuze.com/api/v1/posts/batch \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"posts": [
{ "content": "Post one", "accounts": ["a1b2c3d4-…"], "external_ref": "batch-1" },
{ "content": "Post two", "accounts": ["b7c8d9e0-…"], "external_ref": "batch-2" }
]
}'{
"data": [
{ "index": 0, "status": 201, "post": { "id": "p1a2b3c4-…", "status": "queued" } }
],
"errors": [
{ "index": 1, "status": 422, "title": "Unprocessable Entity", "detail": "Target account not found: b7c8d9e0-…." }
]
}List posts
GET /posts
Returns your posts, newest first. Deleted posts are excluded.
curl https://api.postfuze.com/api/v1/posts \
-H "Authorization: Bearer sk_live_…"{
"data": [
{
"id": "p1a2b3c4-0000-4a9f-9b1c-2e7d6f0a1234",
"status": "scheduled",
"is_draft": false,
"scheduled_at": "2026-06-10T15:00:00.000Z",
"published_at": null,
"created_at": "2026-06-09T12:20:00.000Z"
}
]
}Retrieve a post
GET /posts/{id}
Fetches a post with its full container hierarchy and per-account targets. Returns 404 if not found or deleted. The shape matches the create response above.
curl https://api.postfuze.com/api/v1/posts/p1a2b3c4-0000-4a9f-9b1c-2e7d6f0a1234 \
-H "Authorization: Bearer sk_live_…"{
"id": "p1a2b3c4-0000-4a9f-9b1c-2e7d6f0a1234",
"status": "published",
"is_draft": false,
"scheduled_at": "2026-06-10T15:00:00.000Z",
"published_at": "2026-06-10T15:00:04.000Z",
"default_config": {},
"external_ref": "launch-2026-06-09",
"created_at": "2026-06-09T12:20:00.000Z",
"updated_at": "2026-06-10T15:00:04.000Z",
"containers": [
{ "id": "c_0001", "position": 0, "role": "main", "content": "Launch day is here 🚀 Read the announcement:" }
],
"targets": [
{
"id": "t_0001",
"social_account_id": "a1b2c3d4-…",
"platform": "x",
"status": "published",
"platform_post_id": "1799012345678901234",
"platform_post_url": "https://x.com/acme/status/1799012345678901234",
"error_code": null,
"error_message": null,
"published_at": "2026-06-10T15:00:04.000Z"
}
]
}Update a post
PATCH /posts/{id}
Replaces the editable post body using the same shape as POST /posts. Only drafts, scheduled posts, and queued posts that have not begun publishing can be updated; in-flight or already-published posts return 409/422 rather than being rewritten.
curl -X PATCH https://api.postfuze.com/api/v1/posts/p1a2b3c4-0000-4a9f-9b1c-2e7d6f0a1234 \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"content": "Updated scheduled copy",
"accounts": ["a1b2c3d4-…"],
"scheduled_at": "2026-06-10T16:00:00.000Z"
}'Delete a post
DELETE /posts/{id}
Soft-deletes the post. If it is still scheduled, queued, or a draft, its status is moved to canceled. Content that has already been published to a platform stays live on that platform.
curl -X DELETE https://api.postfuze.com/api/v1/posts/p1a2b3c4-0000-4a9f-9b1c-2e7d6f0a1234 \
-H "Authorization: Bearer sk_live_…"{
"id": "p1a2b3c4-0000-4a9f-9b1c-2e7d6f0a1234",
"canceled": true
}Post analytics
GET /posts/{id}/analytics
Fetches live engagement for each published target from its platform, snapshots it, and returns the latest snapshot per target plus aggregated totals across all targets. Per-account errors (a bad token, an API hiccup, or an adapter without metrics support) are isolated — that account is skipped and the others are still returned.
Returns 404 if the post does not exist or is deleted. For a draft or scheduled post — where nothing has been published yet — it returns 422 Unprocessable Entity(“Analytics are only available after a post is published”) rather than empty arrays.
| Field | Type | Description |
|---|---|---|
post_id | string | The post being measured. |
metrics_by_account[] | object[] | Latest per-target snapshot: target_id, platform, impressions, reach, likes, comments, shares, saves, clicks, video_views, engagement_rate, captured_at. |
aggregated_metrics | object | Sums across targets: total_impressions, total_reach, total_likes, total_comments, total_shares, total_saves, total_clicks, total_video_views. |
curl https://api.postfuze.com/api/v1/posts/p1a2b3c4-0000-4a9f-9b1c-2e7d6f0a1234/analytics \
-H "Authorization: Bearer sk_live_…"{
"post_id": "p1a2b3c4-0000-4a9f-9b1c-2e7d6f0a1234",
"metrics_by_account": [
{
"target_id": "t_0001",
"platform": "x",
"impressions": 12480,
"reach": 9820,
"likes": 421,
"comments": 38,
"shares": 57,
"saves": 12,
"clicks": 304,
"video_views": 0,
"engagement_rate": 0.041,
"captured_at": "2026-06-10T18:00:00.000Z"
}
],
"aggregated_metrics": {
"total_impressions": 12480,
"total_reach": 9820,
"total_likes": 421,
"total_comments": 38,
"total_shares": 57,
"total_saves": 12,
"total_clicks": 304,
"total_video_views": 0
}
}