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 0main — the root post.
  • Position 1first_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 config object: { "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.

ParameterTypeRequiredDescription
contentstringConditionalText of a single-container post. Provide either content or containers.
containersobject[]ConditionalOrdered containers (1–10). Each has content (string) and media (array of { id, alt_text? }).
accountsstring[]YesTarget 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_atstring (ISO 8601)NoWhen to publish. Omit to publish immediately.
queued_from_accountstringNoAssign the post to the next open recurring posting slot for one of the accounts in accounts[]. Mutually exclusive with scheduled_at and drafts.
queue_timezonestring (IANA timezone)NoWall-clock timezone used to resolve queued_from_account slots. Defaults to UTC.
is_draftbooleanNoCreate the post without queuing it. Defaults to false.
configobjectNoPer-platform configuration, keyed by platform identifier. May also be passed as top-level platform keys (see above).
external_refstringNoIdempotency 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
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 }
    }
  }'
201 Created
{
  "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 — add to the next queue slot
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
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" }
    ]
  }'
207 Multi-Status
{
  "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
curl https://api.postfuze.com/api/v1/posts \
  -H "Authorization: Bearer sk_live_…"
200 OK
{
  "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
curl https://api.postfuze.com/api/v1/posts/p1a2b3c4-0000-4a9f-9b1c-2e7d6f0a1234 \
  -H "Authorization: Bearer sk_live_…"
200 OK
{
  "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
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
curl -X DELETE https://api.postfuze.com/api/v1/posts/p1a2b3c4-0000-4a9f-9b1c-2e7d6f0a1234 \
  -H "Authorization: Bearer sk_live_…"
200 OK
{
  "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.

FieldTypeDescription
post_idstringThe 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_metricsobjectSums across targets: total_impressions, total_reach, total_likes, total_comments, total_shares, total_saves, total_clicks, total_video_views.
curl
curl https://api.postfuze.com/api/v1/posts/p1a2b3c4-0000-4a9f-9b1c-2e7d6f0a1234/analytics \
  -H "Authorization: Bearer sk_live_…"
200 OK
{
  "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
  }
}