Publishing
Create a post, target accounts, publish it, and read the per-account result.
Publishing in FoPost is two steps: create a post, then publish it. They are separate because a post is a durable object with per-account deliveries, not a fire-and-forget request. Creating and reading posts needs the posts scope. Publishing, retrying and cancelling also need publish, so a key can manage drafts without being able to send anything to a network.
The model
A post holds your content and the list of accounts it targets. An account is one connected profile, for example your LinkedIn company page. You target account ids, never platform names: a workspace can hold three X accounts, and the API will not guess which one you meant.
Publishing produces one delivery per account. Deliveries succeed and fail independently, which is why a post can end up partially_failed.
Create a post
POST /v1/postscurl -X POST https://api.fopost.com/v1/posts \
-H "X-API-Key: $FOPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "7d2b8c11-4e5a-4a8f-b0d9-3c5e6f7a8b90",
"accounts": ["3a9f1d20-6c77-4b2e-9a01-8f5d2c3b4e10"],
"content": [
{ "text": "Shipping something new today." }
],
"status": "draft"
}'| Field | Type | Required | Description |
|---|---|---|---|
workspace_id | uuid | Yes | Workspace the post belongs to |
accounts | uuid[] | Yes, unless account_group_id is set | Connected accounts to target |
account_group_id | uuid | No | Target every account in an account group. Merged with accounts; each account publishes once |
content | block[] | Yes | One block per item. More than one block makes a thread |
content[].text | string | No | The text of the block |
content[].media | media[] | No | Attachments for the block |
content_type | enum | No | post, thread, or reel. Default post |
artifact_type | enum | No | text_post, thread, article, carousel, short_video, link_share |
status | enum | No | draft or scheduled. Default draft |
schedule_at | datetime | No | When to publish. Required when status is scheduled |
schedule | enum | No | queue takes the next free posting slot instead of a schedule_at |
labels | uuid[] | No | Labels to attach |
title | string | No | Title, for platforms that take one |
summary | string | No | Summary, for platforms that take one |
auto_plug | boolean | No | Post a follow-up comment after publishing |
Media items take type (image, video, gif), name, and url, plus optional alt and thumbnail. Upload files first through the media endpoints and pass what they return.
The response is the created post, with id and status.
Publish it
POST /v1/posts/{id}/publishcurl -X POST https://api.fopost.com/v1/posts/$POST_ID/publish \
-H "X-API-Key: $FOPOST_API_KEY"This answers 202 Accepted and queues delivery. It does not wait for the platforms, so a 202 means accepted, not live. Poll deliveries or subscribe to webhooks for the outcome.
Two options are worth knowing:
accountIdspublishes to a subset of the post's accounts instead of all of themoptions.dryRunvalidates and returns the accounts it would target without publishing, answering200instead of202
Accounts whose credentials have expired or been revoked fail immediately. Accounts flagged degraded still publish, with a warning attached.
Check before you publish
POST /v1/posts/{id}/preflightPreflight returns two different things, and the difference matters:
issuesare hard blockers, such as content over a platform's character limit or a media type the platform will not take- content quality signals are advisory. They never block publishing
Run it when you are publishing on behalf of a user who cannot see the composer.
Read the result
GET /v1/posts/{id}/deliveriesOne record per account, newest first:
{
"data": [
{
"id": "3a9f1d20-6c77-4b2e-9a01-8f5d2c3b4e10",
"platform": "linkedin",
"username": "yourbrand",
"publish_status": "published",
"posted_at": "2026-08-29T14:30:12.000Z",
"external_url": "https://www.linkedin.com/feed/update/urn:li:share:6789",
"attempts": 1,
"max_attempts": 3
},
{
"id": "b81e4f02-91a3-4c55-8de6-77a0c1f2d3e4",
"platform": "twitter",
"username": "yourbrand",
"publish_status": "failed",
"error_code": "media_too_large",
"error_message": "The attached video exceeds the size this platform accepts.",
"attempts": 3,
"max_attempts": 3
}
]
}publish_status is one of pending, queued, delayed, publishing, published, failed, cancelled, unpublished. The post's own status summarises them: published when every delivery landed, partially_failed when some did.
Retry a failure
POST /v1/posts/{id}/retryRe-queues the failed deliveries of a failed, partially failed, or published post. Deliveries that have already used their attempts come back in exceeded and are skipped rather than silently dropped. Resending to an account that already published requires includePublished: true, so you cannot double-post by accident.
Transient failures are already retried automatically with exponential backoff. Reach for this endpoint when the cause was on your side, for example a media URL that was not reachable yet.
Cancel
POST /v1/posts/{id}/cancelCancels a scheduled post and its queued deliveries. Anything already published stays published.
Edit after publishing
PUT /v1/posts/{id}A post that has gone out can still be edited. Send a content change only, content, title, settings, labels, internal_title or summary, and the new text is pushed to every network the post is live on. The accounts and the schedule stay as they are; sending accounts, status or schedule_at on a published post answers 409.
Every network the post is live on has to support editing in place. Each post account carries remote_edit, and when one of them is false the whole update answers 409 with unsupported_platforms and changes nothing, so a caption never drifts between networks. X is the usual reason: its API deletes and reposts, it does not edit.
The response is the post with a remote_edits array, one entry per network with edited and, when a network refused, its error. The pass is also written to the publish log as its own run. Reaching a network needs the publish scope as well as posts.
Unpublish
POST /v1/posts/{id}/unpublishTakes the post down on every network it is live on. The post stays in FoPost as a record: each delivery is marked unpublished, the pass is written to the publish log, and the post status becomes unpublished once nothing is live any more. Requires the publish scope.
Every network has to support deleting a live post (remote_delete on each post account). When one does not, the request answers 409 with unsupported_platforms and removes nothing. A network that refuses at the time comes back with removed: false and keeps its delivery published.
{
"data": {
"post_status": "unpublished",
"deliveries": [
{ "account_id": "…", "platform": "linkedin", "removed": true }
]
}
}To remove the post from FoPost as well, use DELETE /v1/posts/{id}?remove_from_platform=1 instead.
Duplicate
POST /v1/posts/{id}/duplicateCopies a post, its content, and its account targeting into a new draft. Useful for a recurring format you do not want to rebuild each time.
Many at once
POST /v1/posts/bulk applies one action to up to 200 posts in a single transaction: shift moves each schedule_at by offset_minutes, label adds, removes, or replaces labels, and delete soft-deletes them and cancels their queued deliveries.
The whole batch is validated first. An id outside the workspace answers 404 and a post that is no longer a draft or scheduled answers 409, in both cases without changing anything.
For importing a spreadsheet of posts, see Scheduling.
Next
Related documentation
- API Overview
Base URL, envelopes, pagination, and errors for the FoPost REST API.
- Authentication
API keys, scopes, and workspace binding.
- Scheduling
Schedule a post, repeat it, and import a batch from a spreadsheet.
- Media
Upload files, list the media library, and attach media to a post.
- Validation
Check content, text length, and media against platform rules before a post exists.
- Accounts
List connected social accounts, check their health, and refresh credentials.
- Workspaces
Workspaces, labels, and how isolation works across them.
- Analytics
Overview totals, time series, top posts, demographics, and label roll-ups.