Publishing
Connect a rendered video to a publishing profile, pick platforms, and read the publish state including which platforms had no account.
Publishing sends a rendered video to the social accounts a user connected in the ShortFast app. Two things decide where it goes: a profile (a named destination holding the connected accounts) and a list of platforms.
Profiles and accounts
A profile is created and connected in the app, not through the API. GET /profiles lists the ones this workspace holds, each with the accounts connected
to it. It is unpaginated: a workspace holds a handful at most.
curl https://app.shortfast.com/api/v1/profiles \
-H "Authorization: Bearer sf_live_xxxxxxxxxxxxxxxxxxxxxxxx"{
"data": [
{
"id": "pR4kM8xT2vQ6nL9wz",
"name": "Acme brand",
"description": "Main brand accounts",
"color": "#0A66C2",
"isDefault": true,
"createdAt": "2026-05-14T08:31:00.000Z",
"accounts": [
{ "id": "acc_tt_01", "platform": "tiktok", "username": "acmehq", "displayName": "Acme", "pictureUrl": "https://cdn.shortfast.com/uploads/avatars/acmehq.jpg" },
{ "id": "acc_yt_01", "platform": "youtube", "username": "@acme", "displayName": "Acme Studio", "pictureUrl": null }
]
}
]
}accounts is cached. Pass ?refresh=true (or ?refresh=1) on /profiles or
/profiles/{id} to re-read them upstream first. That is slower, and it never
fails the request: if the accounts cannot be re-read, the cached ones are
returned unchanged. Use it before a publish that depends on an account the user
just connected.
Platforms
ShortFast publishes to TikTok, YouTube and Instagram: those are the only accounts the app lets a user connect to a profile.
GET /platforms returns every platform key the publishing API accepts. These
are exactly the values platforms accepts on a publish and on an
automation's destination. The list is static and
workspace-independent, so read it once at integration time rather than per call.
| Key | Name | Connectable in ShortFast |
|---|---|---|
tiktok | TikTok | Yes |
youtube | YouTube | Yes |
instagram | Yes | |
twitter | Twitter/X | No |
facebook | No | |
linkedin | No | |
threads | Threads | No |
pinterest | No | |
reddit | No | |
bluesky | Bluesky | No |
A platform key is not a promise that an account exists for it. Whether a profile
can actually reach a platform is decided per publish, from the accounts connected
to that profile. Since no ShortFast account can be connected for the last seven
keys, a publish that names one of them always reports it in
missingPlatforms.
Check before publishing
GET /videos/{id}/publish answers three questions at once: can this video be
published, where would a publish send it, and what happened last time.
curl https://app.shortfast.com/api/v1/videos/7bQxL2mF9dR4tK1sv/publish \
-H "Authorization: Bearer sf_live_xxxxxxxxxxxxxxxxxxxxxxxx"{
"data": {
"videoId": "7bQxL2mF9dR4tK1sv",
"rendered": true,
"destination": { "profileId": "pR4kM8xT2vQ6nL9wz", "platforms": ["tiktok", "youtube"] },
"publish": null
}
}rendered is true once a render exists; a video can only be published with
one. destination is what a publish with no explicit body would use: a clip
inherits its blueprint's destination, so an automation's clips need no
configuration of their own. publish is null until the first attempt.
Publish a video
curl -X POST https://app.shortfast.com/api/v1/videos/7bQxL2mF9dR4tK1sv/publish \
-H "Authorization: Bearer sf_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9a1c4d77-52b6-4e0f-8c33-71d0e2f9b845" \
-d '{
"profileId": "pR4kM8xT2vQ6nL9wz",
"platforms": ["tiktok", "youtube"],
"caption": "Burnout is not a workload problem. #founders"
}'Every field is optional. Omit profileId and platforms to use the video's own
destination, and caption to use the video's brief.description (caption is
capped at 5000 characters, comfortably above every platform's own limit).
Duplicate platform keys are collapsed, and an unknown one is a 400 before
anything is sent.
Always send an Idempotency-Key. A publish is the one action with a visible,
irreversible side effect on someone else's timeline, and a retry after a client
timeout would post the same video twice.
missingPlatforms and unhealthyPlatforms
A requested platform that cannot be reached does not fail the call. It is reported back instead, so the platforms that could be reached still go out:
{
"data": {
"videoId": "7bQxL2mF9dR4tK1sv",
"platforms": ["tiktok"],
"missingPlatforms": ["youtube"],
"unhealthyPlatforms": ["instagram"],
"publish": {
"status": "COMPLETED",
"scheduledAt": null,
"startedAt": "2026-08-06T09:40:02.000Z",
"publishedAt": "2026-08-06T09:40:31.000Z",
"failedAt": null,
"platforms": ["tiktok"],
"missingPlatforms": ["youtube"],
"unhealthyPlatforms": ["instagram"],
"caption": "Burnout is not a workload problem. #founders",
"error": null,
"stats": null
}
}
}platforms is what the video actually reached. missingPlatforms had no
connected account at all, and unhealthyPlatforms had one that lost access to
the network and has to be reconnected before it can be posted to again. Treat
either as a partial success worth surfacing: the publish is COMPLETED and will
not retry those platforms on its own.
The call fails outright only when nothing is left to post to: that is
warning-publish-no-accounts when no platform had an account, and
warning-publish-auth when every one of them needs reconnecting.
Knowing before you publish
GET /profiles carries the state of every connected account, so an integration
never has to discover a dead one by having a publish skip it:
{
"id": "acc_ig_01",
"platform": "instagram",
"username": "acmehq",
"health": {
"status": "needs_reconnect",
"reason": "Your instagram access token is no longer valid. Please reconnect your account."
}
}status is healthy or needs_reconnect, and reason carries the network's own
explanation when it gave one. accountsSyncedAt on the profile says when that was
last read; pass ?refresh=true to re-read it first. Only the person who owns the
account can reconnect it, in the app under Settings, so the useful thing an
integration can do is stop scheduling to it and tell them.
The publish state machine
publish.status moves through four values, and the timestamps say when each
transition happened:
| Status | Meaning | Timestamp |
|---|---|---|
SCHEDULED | An automation slotted this clip to go out later | scheduledAt |
PUBLISHING | The post is in flight | startedAt |
COMPLETED | The video is live on platforms | publishedAt |
FAILED | The attempt stopped. error.code says why | failedAt |
DELETE /videos/{id}/publish wipes the block so the video can be sent again. It
is refused with warning-invalid-input while a publish is live or already
finished: clearing it then would lose the result and let an automation post the
same video twice.
Publish stats
ShortFast reads a published post back from the platforms hourly for the first
two days after publishing, then once a day, until 30 days after publishing.
publish.stats is what the platforms show: per platform, whether the post went
live, its link, and its views, likes, comments and shares. It is null until
the first read-back.
| Field | Meaning |
|---|---|
checkedAt | When ShortFast last read the post back |
updatedAt | When the platforms last updated the counts |
available | false once the post can't be read any more. The counts are then the last ones seen |
totals | views, likes, comments and shares, added up across the platforms |
platforms | One entry per platform: platform, status, url, error and the same four counts |
A platform's status is published, publishing while the platform is still
processing the upload, or failed, with the platform's reason in error.
url is the post on the platform, once it has a link. A count is null until
the platform reports it, and YouTube reports no shares.
A post that every platform refused turns the publish from COMPLETED to
FAILED, usually within the hour, with warning-publish-rejected in
publish.error and a video.publish_failed webhook.
publish.stats then says why, per platform.
When a publish is refused
| Code | Why | What clears it |
|---|---|---|
warning-plan-publish | Publishing needs a paid plan. Downloads stay free | Upgrade |
warning-publish-not-configured | No render yet, no profile selected, no platforms selected, or the profile no longer exists | Fix the destination |
warning-publish-no-accounts | None of the requested platforms has a connected account | Connect an account |
warning-publish-auth | Every requested platform's account lost access to the network | Reconnect it in Settings |
warning-publish-duplicate | The same video and caption already went to that account in the last 24 hours | Change the caption or the video |
warning-publish-rate-limited | The platform is limiting how often this account can post, such as YouTube's daily limit | Post again after the error's retryAt, when the platform gave one |
warning-publish-rejected | The platform refused the post with a reason, such as a video too long for it | Fix what the error's message names |
The same codes come back on publish.error.code when an attempt fails after it
started, and on the video.publish_failed webhook. Subscribe to
video.published and video.publish_failed rather than polling: a publish
triggered by an automation happens without any call from you.