Overview

Publishing Posts

Create, schedule, cancel and delete posts on one or more connected accounts, and track each destination's status.

One POST /v1/posts request publishes the same content to up to 50 connected accounts. This page covers the request, the post types, the statuses you read back, and how to schedule, cancel and delete.

Create a Post

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "New release notes are up.",
    "mediaUrls": [],
    "mediaKind": "text",
    "destinations": [
      {
        "accountId": "YOUR_X_ACCOUNT_ID"
      },
      {
        "accountId": "YOUR_BLUESKY_ACCOUNT_ID"
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "New release notes are up.",
    "mediaUrls": [],
    "mediaKind": "text",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "accountId": "YOUR_X_ACCOUNT_ID",
        "platform": "x",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://x.com/you/status/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      },
      {
        "accountId": "YOUR_BLUESKY_ACCOUNT_ID",
        "platform": "bluesky",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://bsky.app/profile/you/post/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

Prop

Type

Media Kinds

mediaKind is required on every post and decides how many media URLs it takes.

mediaKindPublishesmediaUrlsPlatforms
textA text-only postNoneX, Facebook, Threads, LinkedIn, Bluesky
imageOne imageExactly 1X, Instagram, Facebook, Threads, LinkedIn, TikTok, Pinterest, Bluesky
multi-imageA carousel or image set2 or more, up to the platform maximumX, Instagram, Facebook, Threads, LinkedIn, TikTok, Bluesky
videoOne videoExactly 1X, Instagram, Facebook, Threads, LinkedIn, YouTube, TikTok, Pinterest, Bluesky
reelA Facebook ReelExactly 1 vertical videoFacebook
documentA document postExactly 1LinkedIn
storiesA storyExactly 1 image or videoInstagram, Facebook

The maximum images per post on each platform is in Supported Platforms. How a kind maps to each platform's own post types is on that platform's posts page.

Per-Destination Overrides

Each entry in destinations can carry fields that apply to that account only:

  • text replaces the shared text for this destination.
  • Platform fields, such as privacyLevel on TikTok or boardId on Pinterest, set platform options. Every field and the platforms that accept it are in Platform Settings.
import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "Behind the scenes at the studio.",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/studio-tour.mp4"
    ],
    "mediaKind": "video",
    "destinations": [
      {
        "accountId": "YOUR_INSTAGRAM_ACCOUNT_ID",
        "firstComment": "Full tour on our site."
      },
      {
        "accountId": "YOUR_TIKTOK_ACCOUNT_ID",
        "text": "Studio tour #behindthescenes",
        "privacyLevel": "PUBLIC_TO_EVERYONE"
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Behind the scenes at the studio.",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/studio-tour.mp4"
    ],
    "mediaKind": "video",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "accountId": "YOUR_INSTAGRAM_ACCOUNT_ID",
        "platform": "instagram",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://www.instagram.com/p/17895695668004550/",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      },
      {
        "accountId": "YOUR_TIKTOK_ACCOUNT_ID",
        "platform": "tiktok",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://www.tiktok.com/@you/video/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

TikTok destinations require privacyLevel, and Pinterest destinations require boardId. A field sent to a platform that does not accept it returns 400 VALIDATION_ERROR.

Cross-Posting Rules

Onepostly checks the whole request against every destination before it accepts the post. If one destination cannot take it, the request returns 400 VALIDATION_ERROR and nothing is published.

  • mediaKind must be supported by every destination's platform. A text post cannot include Instagram, YouTube, TikTok or Pinterest.
  • The shared text must fit the text limit of every destination that does not set its own text. To post longer text on one platform, set text on that destination.
  • Each threadItems entry must fit the text limit of every destination.

Media is checked when each destination publishes. Media that one platform rejects, such as too many images or a video that is too long, fails only that destination with an errorCode. Check mediaUrls against every destination's media requirements on its posts page and in Media Uploads.

Text limits per platform are in Platform Settings.

Post Status

A new post returns 202 Accepted with status: "queued", or 201 Created with status: "scheduled" when it has a future scheduledFor. Read it back with GET /v1/posts/{id}:

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.getPost({
  id: "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Ship day",
    "mediaUrls": [
      "https://cdn.onepostly.com/media/product.jpg"
    ],
    "mediaKind": "image",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5",
        "platform": "x",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://x.com/you/status/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

Each destination has its own status. When a destination is published, externalPostId and externalUrl identify the post on the platform. On some platforms externalUrl fills in a few minutes after publishing. When a destination is failed, errorCode and errorMessage say why.

Destination statusMeaning
draftSaved as a draft in the dashboard. Not published
scheduledWaiting for its scheduled time
queuedAccepted and about to publish
processingPublishing to the platform now
publishedLive on the platform. Final unless you delete it
failedNot published. Final
cancelledCancelled before publishing. Final
deletedRemoved from the platform through remote delete. Final

The post status summarizes its destinations:

Post statusWhen
draft, scheduled, queued, processingAt least one destination still has this status. The most advanced one wins, in the order processing, queued, scheduled, draft
publishedEvery destination finished, at least one is published and none failed
partialEvery destination finished, some published and some failed
failedEvery destination finished, none published and at least one failed
cancelledEvery destination finished without publishing, and at least one was cancelled
deletedEvery destination is deleted

A failed destination is not published again. To retry, fix the cause and create a new post for that account. To receive these changes instead of reading the post, subscribe to post webhooks such as post.platform.published, post.platform.failed and post.published.

Scheduling

Set scheduledFor to a local date and time and timezone to its IANA zone. Omitted, timezone is UTC.

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "Join our product webinar next week.",
    "mediaUrls": [],
    "mediaKind": "text",
    "destinations": [
      {
        "accountId": "YOUR_LINKEDIN_ACCOUNT_ID"
      }
    ],
    "scheduledFor": "2027-03-15T09:00:00",
    "timezone": "America/New_York"
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Join our product webinar next week.",
    "mediaUrls": [],
    "mediaKind": "text",
    "status": "scheduled",
    "scheduledFor": "2027-03-15T09:00:00",
    "timezone": "America/New_York",
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "YOUR_LINKEDIN_ACCOUNT_ID",
        "platform": "linkedin",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://www.linkedin.com/feed/update/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}
  • The response is 201 Created with status: "scheduled".
  • scheduledFor can be up to 1 year ahead.
  • A time less than 1 minute ahead publishes right away and returns 202 Accepted.
  • A time more than 5 minutes in the past returns 400 VALIDATION_ERROR.

Cancel a Post

Cancel a post that is scheduled, queued or processing:

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.cancelPost({
  id: "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
});
console.log(response);
{
  "post": {
    "id": "1699f415-7fb6-43f4-9d2a-c447491f32a8",
    "text": "Join our product webinar next week.",
    "mediaUrls": [],
    "mediaKind": "text",
    "status": "cancelled",
    "scheduledFor": "2027-03-15T09:00:00",
    "timezone": "America/New_York",
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "YOUR_LINKEDIN_ACCOUNT_ID",
        "platform": "linkedin",
        "status": "cancelled",
        "externalPostId": null,
        "externalUrl": null,
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": null,
        "metrics": null
      }
    ],
    "createdAt": "2027-03-01T12:00:00.000Z",
    "updatedAt": "2027-03-02T08:30:00.000Z"
  }
}

Destinations that are scheduled or queued become cancelled. A destination that is already processing finishes publishing, so the post can still end up published or partial. Cancelling does not remove posts that are already live; use remote delete for that.

Remote Delete

Delete a published post from the platform with the destination id from the post response:

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.deletePostDestination({
  destinationId: "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
});
console.log(response);
{
  "post": {
    "id": "1699f415-7fb6-43f4-9d2a-c447491f32a8",
    "text": "New release notes are up.",
    "mediaUrls": [],
    "mediaKind": "text",
    "status": "deleted",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
        "accountId": "YOUR_X_ACCOUNT_ID",
        "platform": "x",
        "status": "deleted",
        "externalPostId": "1893420157284917248",
        "externalUrl": "https://x.com/yourbrand/status/1893420157284917248",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2027-03-15T13:00:04.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2027-03-15T13:00:00.000Z",
    "updatedAt": "2027-03-16T10:00:00.000Z"
  }
}
  • The destination becomes deleted. Other destinations of the post are not affected.
  • A thread is deleted completely, including its replies.
  • A failed destination can be deleted when part of it went live, such as the first posts of a thread.
  • Deleting a destination that is already deleted returns the post unchanged.
  • On X, each deleted post is paid from your wallet.

Which platforms support remote delete is in Supported Platforms.

Threads and Quotes

These fields work on X, Threads and Bluesky:

FieldWhereEffect
threadItemsRequest bodyPublishes each item as a reply under the post, in order. 1 to 25 items, each with content and up to 20 mediaItems
quoteTweetIdDestinationQuotes an existing post: the post id on X and Threads, an at:// URI on Bluesky
replyToIdDestinationPublishes the post as a reply to an existing post: the post id on Threads, an at:// URI on Bluesky. Threads and Bluesky only

The field is named quoteTweetId on every platform. The top-level text, mediaKind and mediaUrls form the first post of a thread.

import { Configuration, PostsApi } from "@onepostly/sdk";

const postsApi = new PostsApi(
  new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);

const response = await postsApi.createPost({
  createPostBody: {
    "text": "Three things we shipped this month.",
    "mediaUrls": [],
    "mediaKind": "text",
    "destinations": [
      {
        "accountId": "YOUR_X_ACCOUNT_ID"
      },
      {
        "accountId": "YOUR_THREADS_ACCOUNT_ID"
      }
    ],
    "threadItems": [
      {
        "content": "1. Scheduling in any IANA time zone.",
        "mediaItems": []
      },
      {
        "content": "2. Remote delete for whole threads.",
        "mediaItems": []
      }
    ]
  },
});
console.log(response);
{
  "post": {
    "id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
    "text": "Three things we shipped this month.",
    "mediaUrls": [],
    "mediaKind": "text",
    "status": "queued",
    "scheduledFor": null,
    "timezone": null,
    "destinations": [
      {
        "accountId": "YOUR_X_ACCOUNT_ID",
        "platform": "x",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://x.com/you/status/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      },
      {
        "accountId": "YOUR_THREADS_ACCOUNT_ID",
        "platform": "threads",
        "status": "draft",
        "externalPostId": "17895695668004550",
        "externalUrl": "https://www.threads.com/@you/post/17895695668004550",
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": "2026-08-25T10:00:00.000Z",
        "metrics": null
      }
    ],
    "createdAt": "2026-08-25T10:00:00.000Z",
    "updatedAt": "2026-08-25T10:00:00.000Z"
  }
}

Platform details are in the X, Threads and Bluesky posts pages.

Errors

CodeHTTPCauseFix
VALIDATION_ERROR400A field is missing or invalid, or the request breaks a cross-posting ruleCorrect the field named in param and the message
CONNECTION_INACTIVE400A destination account is not activeReconnect the account
TOKEN_INVALID400Remote delete: the account's access was revokedReconnect the account, then try again
UNSUPPORTED_PLATFORM400Remote delete is not available on this platformDelete the post on the platform
MEDIA_DELETE_UNSUPPORTED400Remote delete of Instagram media on an account connected with Instagram LoginReconnect with Facebook Login, or delete the post in Instagram
PAYMENT_METHOD_REQUIRED402The workspace needs a payment methodAdd a payment method in Settings → Billing
PAYMENT_PAST_DUE402The workspace has an unpaid invoiceUpdate the payment method in Settings → Billing
INSUFFICIENT_WALLET402Remote delete on X: the wallet balance is too lowTop up the wallet in Settings → Billing
FORBIDDEN403A read_only API keyUse a read_write key
NOT_FOUND404The post does not exist in this workspaceCheck the post id
ACCOUNT_NOT_FOUND404A destination accountId is not a connection in this workspaceList connections and use a current id
DESTINATION_NOT_FOUND404The destination id does not existUse destinations[].id from the post
NOT_CANCELLABLE409The post is not scheduled, queued or processing, or none of its destinations can still be cancelledRead the post. Use remote delete for published destinations
NOT_DELETABLE409The destination has no live post on the platformOnly delete destinations that are published, or failed with an externalPostId
RATE_LIMITED429Too many posts published to one account in a short timeWait, then retry. See Rate Limits
QUOTA_EXCEEDED429Remote delete: the platform's delete limit for the account is reachedTry again later. See Rate Limits
DELETE_FAILED502The platform refused or failed the deleteTry again. If it persists, delete the post on the platform
PLATFORM_UNAVAILABLE502The platform did not respond in timeTry again

Errors on a single destination during publishing appear in its errorCode, not as an HTTP error. They are listed on each platform's posts page.