API reference

Endpoints, fields and answers, generated from the same OpenAPI file the server uses.

Base URL https://postsail.app/api; every path starts with /v1. Send the API key as Authorization: Bearer …. Generated from the OpenAPI file (/api/v1/openapi.json), version 1.0.0-beta.1.

Fields for YouTube, TikTok, Instagram and Facebook are accepted by the API, but connecting those platforms is not open yet. Today you can connect X and Bluesky accounts.

Managing API keys, members, invitations and workspaces needs a dashboard sign-in, so those endpoints are not listed here; they are in the OpenAPI file.

Posts

POST /v1/posts

Create a post

Creates one post with one target per account and returns it right away (status: queued). Publishing happens in the background at publish_at (now if omitted); poll GET /v1/posts/{post_id} for each target's result.

  • Retrying this request is safe with Idempotency-Key or external_id (see the top of this document).
  • dry_run: true runs every check and stores dry_preview per target; nothing is published.
  • publish_before: the post is dropped (expired) instead of going out late.
  • Text, audio name and media are checked against platform rules and blocked words before the queue.

Parameters

NameInTypeDescription
Idempotency-KeyheaderstringAny unique string for this request (1-255 visible ASCII characters). Kept 30 days.

Request body

FieldTypeDescription
external_idstringYour own id for this post. The same external_id never creates a second post.
media_urlstringHTTPS URL of the video. Must start with one of the workspace's allowed prefixes.
publish_atstringWhen to publish (ISO 8601). Omit for now; at most 30 days ahead.
publish_beforestringDrop the post (status expired) instead of publishing it after this time.
dry_runbooleanRun every check and store what would be sent, without calling any platform.
media_upload_idstringId from POST /v1/uploads, after the file was uploaded (instead of media_url).
media_sha256stringSHA-256 of the approved video: if the fetched video differs, no target is published.
targets requiredarray of objectOne entry per account; each account at most once.
targets[].account requiredstringAccount slug from GET /v1/accounts.
targets[].textstringCaption or post text for this account. Required when the post has no media_url. On X, links are removed from this text (put them in first_comment).
targets[].draftbooleanFacebook only: create the reel as a draft that only page admins see.
targets[].audio_namestringInstagram Reels only: name of the reel's original audio.
targets[].ai_generatedbooleanAI disclosure: YouTube containsSyntheticMedia, Instagram and Facebook is_ai_generated.
targets[].first_commentstringComment written after publishing (YouTube, Instagram, Facebook, X). On X it is a reply to the post and may contain links; links in the main X text are removed.
targets[].titlestringVideo title: required on YouTube, optional on Facebook.
targets[].cover_msintegerInstagram Reels only: cover frame, ms from the start of the video.
targets[].visibilitystring: public, unlisted, privateYouTube only: who can see the video (public, unlisted or private). Default public.
targets[].made_for_kidsbooleanYouTube only: the video is made for kids (YouTube's "Made for Kids"). Default false.
targets[].privacy_levelstring: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLYTikTok only: who can view the video. Required for a direct post; pick one of the account's privacy_level_options (GET /v1/accounts/{account}/creator_info). There is no default.
targets[].allow_commentbooleanTikTok only: allow comments. Default false.
targets[].allow_duetbooleanTikTok only: allow Duets. Default false.
targets[].allow_stitchbooleanTikTok only: allow Stitches. Default false.
targets[].brand_organicbooleanTikTok only: the video promotes your own brand, product or service (labeled "Promotional content").
targets[].brand_contentbooleanTikTok only: paid partnership promoting another brand (labeled "Paid partnership"). Cannot be private.

Responses

  • 200 The same request was sent before: the existing post is returned and nothing new is created. Returns Post.
  • 201 Post created. Returns Post.
  • 400 Malformed Idempotency-Key. Codes: invalid_idempotency_key. Returns Error.
  • 401 Missing, unknown or revoked API key. Codes: unauthorized. Returns Error.
  • 402 Over the plan's monthly limit; details has limit, allowed, used, requested and resets_at. See GET /v1/usage. Codes: plan_limit_reached. Returns Error.
  • 403 The API key may not post to these accounts; details lists them. Codes: account_not_allowed. Returns Error.
  • 409 The same Idempotency-Key or external_id was used with a different body; details.post_id when visible. Codes: idempotency_key_reused, external_id_exists. Returns Error.
  • 422 The body failed validation; details lists each issue. Codes: validation_failed. Returns Error.
  • 429 More than 30 new posts in this hour for this API key. Codes: rate_limited. Returns Error.

GET /v1/posts

List posts

Latest publish_at first. Filters combine. Follow next_cursor until it is null. A key limited to some accounts sees only posts whose every target it may use, so a page can be shorter than limit even when more pages follow.

With external_id this looks up one post instead (see the parameter).

Parameters

NameInTypeDescription
statusquerystring: queued, publishing, published, partial, failed, dry_done, canceledOnly posts with this status.
accountquerystringOnly posts with a target on this account.
fromquerystringOnly posts with publish_at at or after this time (ISO 8601).
toquerystringOnly posts with publish_at before this time (ISO 8601).
limitqueryintegerPage size, 1-100.
cursorquerystringnext_cursor from the previous page.
external_idquerystringLook up the one post with this external_id instead of listing, e.g. after an unclear response to createPost. The response is then a single Post (or 404). Cannot be combined with the other parameters.

Responses

  • 200 A page of posts; with external_id, the one matching post.
  • 400 A query parameter is invalid; details lists each issue. Codes: invalid_query, missing_external_id. Returns Error.
  • 401 Missing, unknown or revoked API key. Codes: unauthorized. Returns Error.
  • 403 The API key may not use this account. Codes: account_not_allowed. Returns Error.
  • 404 With external_id: no post with this external_id for this app, or not visible to this key. Codes: not_found. Returns Error.

GET /v1/posts/{post_id}

Get a post and its targets

Parameters

NameInTypeDescription
post_id requiredpathstring

Responses

  • 200 The post. Returns Post.
  • 401 Missing, unknown or revoked API key. Codes: unauthorized. Returns Error.
  • 404 Unknown post, or it targets an account this key may not use. Codes: not_found. Returns Error.

POST /v1/posts/{post_id}/cancel

Cancel a post

Stops every target that has not gone out yet: queued, needs_reauth and needs_approval become canceled. Posts never change; to fix one, cancel it and create a new post with a new external_id.

  • A target being published right now cannot be stopped. If it fails temporarily, it is canceled before the next attempt.
  • Published and failed targets stay as they are; failed targets of a canceled post can no longer be retried.
  • Canceled targets do not count against the plan.
  • Canceling again returns the same post.

Parameters

NameInTypeDescription
post_id requiredpathstring

Responses

  • 200 Canceled; the post is returned. Returns Post.
  • 401 Missing, unknown or revoked API key. Codes: unauthorized. Returns Error.
  • 404 Unknown post, or it targets an account this key may not use. Codes: not_found. Returns Error.
  • 409 Nothing left to cancel: every target was published, failed or is being published; details.status is the post's status. Codes: not_cancelable. Returns Error.

POST /v1/posts/{post_id}/targets/{account}/retry

Retry a failed target

Queues a failed or needs_reauth target again while publish_before has not passed. The attempt counter restarts; a half-finished upload continues where it stopped and is not posted twice. Not retryable: validation_failed, blocked_content, platform_not_supported, x_duplicate, media_required, stopped_by_operator, and posts whose video could not be fetched.

Parameters

NameInTypeDescription
post_id requiredpathstring
account requiredpathstring

Responses

  • 202 Queued again; the post is returned. Returns Post.
  • 401 Missing, unknown or revoked API key. Codes: unauthorized. Returns Error.
  • 404 Unknown post or target, or not visible to this key. Codes: not_found. Returns Error.
  • 409 This target cannot be retried (details has its status and error code), or the post was canceled. Codes: not_retryable, post_canceled. Returns Error.

POST /v1/uploads

Upload a video file

Use this when your video is not at a public HTTPS URL. Three steps: 1. POST /v1/uploads with the file's type and size; you get upload_url and id. 2. Send the file to upload_url with the method and headers given, before expires_at. 3. Create the post with media_upload_id: id instead of media_url. The file is checked (length, size, format) like any other video when the post is prepared.

Request body

FieldTypeDescription
content_type requiredstring: video/mp4, video/quicktimeThe video's type: video/mp4 or video/quicktime.
size_bytes requiredintegerFile size in bytes, at most 104857600.

Responses

  • 201 Upload started. Returns UploadStart.
  • 401 Missing, unknown or revoked API key. Codes: unauthorized. Returns Error.
  • 422 The type or size is not accepted. Codes: validation_failed. Returns Error.
  • 501 Direct uploads are not available on this server. Codes: uploads_unavailable. Returns Error.

Accounts

GET /v1/accounts

List accounts this key can post to

Responses

  • 200 Accounts, sorted by slug. Returns AccountList.
  • 401 Missing, unknown or revoked API key. Codes: unauthorized. Returns Error.

GET /v1/accounts/{account}/creator_info

TikTok: the creator's posting options

TikTok is in review: connecting TikTok accounts is not open yet.

TikTok accounts only. Read this right before posting and show it to the user: the account's nickname and avatar, the privacy_level_options to choose from (no default), and whether comments, Duets or Stitches are off in the creator's TikTok settings. A video longer than max_video_post_duration_sec cannot be posted to this account.

Parameters

NameInTypeDescription
account requiredpathstring

Responses

  • 200 The creator's posting options. Returns TiktokCreatorInfo.
  • 401 Missing, unknown or revoked API key. Codes: unauthorized. Returns Error.
  • 404 Unknown account, or this key may not use it. Codes: account_not_found. Returns Error.
  • 409 Not a TikTok account, not connected, needs reconnecting, or TikTok is not available yet. Codes: not_tiktok, account_not_connected, account_needs_reauth, platform_not_available. Returns Error.
  • 502 TikTok returned an error. Codes: tiktok_error. Returns Error.
  • 503 TikTok did not answer; retry after a minute. Codes: tiktok_unavailable. Returns Error.

Usage

GET /v1/usage

Plan limits and this month's count

Posts count per target: a post to three accounts counts three. Dry runs and the sandbox account do not count; canceled targets are given back. The count belongs to the workspace, so every key sees the same numbers. A post over the limit gets 402 plan_limit_reached.

Responses

  • 200 Plan, limits and usage. Returns Usage.
  • 401 Missing, unknown or revoked API key. Codes: unauthorized. Returns Error.

Schemas

Post

A post and the state of each target.

FieldTypeDescription
idstringpst_…
external_idstring or null
statusstring: queued, publishing, published, partial, failed, dry_done, canceledSummary of the targets: partial when some targets were published and some failed. Canceled targets are left out of the summary; canceled when every target was canceled.
dry_runboolean
media_urlstring or null
publish_atstring
publish_beforestring or null
created_atstring
canceled_atstring or nullWhen the post was canceled (POST /v1/posts/{post_id}/cancel); null if it was not.
targetsarray of Target

PostList

One page of posts.

FieldTypeDescription
postsarray of PostLatest publish_at first.
next_cursorstring or nullPass as cursor to get the next page; null on the last page.

Target

One account of a post.

FieldTypeDescription
accountstringAccount slug in this workspace.
platformstring: facebook, instagram, x, telegram, youtube, tiktok, linkedin, bluesky, sandbox
textstring or null
draftboolean
audio_namestring or null
dry_runbooleantrue when this target never calls the platform: the post is a dry run or the account is not live.
statusstring: queued, publishing, published, failed, expired, needs_reauth, blocked_content, needs_approval, dry_done, canceledqueued → publishing → published. Dry run ends in dry_done. Failures: failed, expired (publish_before passed), blocked_content, needs_reauth (reconnect the account). canceled: the post was canceled before this target went out.
platform_post_idstring or null
permalinkstring or null
errorTargetError or null
dry_previewDryPreview or nullDry run only: what would have been sent.

TargetError

Why a target failed or is being retried.

FieldTypeDescription
codestringMachine-readable reason, e.g. sandbox_rejected, blocked_content, attempts_exhausted:platform_unavailable.
messagestringHuman-readable detail. Never contains tokens.
retryablebooleantrue while the error is temporary and the target is being retried automatically.

DryPreview

What a dry-run target would have sent.

FieldTypeDescription
textstring or null
audio_namestring or null
mediaMediaSummary or null
atstring

MediaSummary

FieldTypeDescription
sha256string
sizeintegerBytes.
durationSecnumber or null
widthinteger or null
heightinteger or null
videoCodecstring or null
audioCodecstring or null

Account

FieldTypeDescription
accountstringSlug used in targets[].account.
platformstring: facebook, instagram, x, telegram, youtube, tiktok, linkedin, bluesky, sandbox
handlestring
statusstring: connected, needs_reauth, disabled, disconnected
livebooleanfalse: every post to this account is a dry run.
purge_atstring or nulldisconnected accounts only: when the account's remaining data is deleted.

AccountList

FieldTypeDescription
accountsarray of Account

Usage

The workspace's plan, its limits and this month's count.

FieldTypeDescription
planstringPlan id, e.g. beta.
periodobjectThe counting period: the current calendar month in UTC.
period.startstring
period.endstring
limitsobject
limits.accountsinteger or nullConnected accounts; the sandbox account and disabled accounts do not count. null: no limit.
limits.posts_per_monthinteger or nullTargets per period: a post to three accounts counts three. Dry runs and the sandbox account do not count; a canceled target is given back. null: no limit.
limits.x_posts_per_monthinteger or nullOf those, targets on X; a first comment on X (a reply) counts one more. null: no limit.
limits.x_link_comments_per_monthinteger or nullFirst comments with links on X (X bills a post with a link 13 times more). null: no limit.
usedobject
used.accountsinteger
used.postsinteger
used.x_postsinteger
used.x_link_commentsinteger

UploadStart

Where to upload the video file, and the id to use as media_upload_id.

FieldTypeDescription
idstringPass this as media_upload_id when you create the post.
upload_urlstringUpload the file here with the given method and headers, before expires_at.
method"PUT"
headersobjectSend exactly these headers with the upload.
expires_atstringISO 8601 time when upload_url stops working.

TiktokCreatorInfo

FieldTypeDescription
accountstring
platform"tiktok"
usernamestring or nullThe creator's TikTok username (without @).
nicknamestring or nullThe creator's display name. Show it next to the post so the user knows which account posts.
avatar_urlstring or null
privacy_level_optionsarray of string: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLYWhat this account can use as privacy_level. There is no default: the user picks one.
comment_disabledbooleanComments are off in the creator's TikTok settings: allow_comment cannot turn them on.
duet_disabledboolean
stitch_disabledboolean
max_video_post_duration_secnumber or nullLongest video this account can post.

Error

Every error response has this shape.

FieldTypeDescription
errorobject
error.codestring
error.messagestring
error.detailsarray of ValidationIssue or array of string or objectvalidation_failed, invalid_query: list of issues. account_not_allowed: account slugs. Conflicts: {post_id}. plan_limit_reached: {limit, allowed, used, requested, resets_at}.

ValidationIssue

FieldTypeDescription
pathstringField path, e.g. targets.0.text.
messagestring