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
GET /platforms returns the platform keys a profile can publish to. 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 |
|---|---|
tiktok | TikTok |
youtube | YouTube |
instagram | |
twitter | Twitter/X |
facebook | |
linkedin | |
threads | Threads |
pinterest | |
reddit | |
bluesky | Bluesky |
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.
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
}
}
}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.
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 |
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.