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.
mediaKind | Publishes | mediaUrls | Platforms |
|---|---|---|---|
text | A text-only post | None | X, Facebook, Threads, LinkedIn, Bluesky |
image | One image | Exactly 1 | X, Instagram, Facebook, Threads, LinkedIn, TikTok, Pinterest, Bluesky |
multi-image | A carousel or image set | 2 or more, up to the platform maximum | X, Instagram, Facebook, Threads, LinkedIn, TikTok, Bluesky |
video | One video | Exactly 1 | X, Instagram, Facebook, Threads, LinkedIn, YouTube, TikTok, Pinterest, Bluesky |
reel | A Facebook Reel | Exactly 1 vertical video | |
document | A document post | Exactly 1 | |
stories | A story | Exactly 1 image or video | Instagram, 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:
textreplaces the sharedtextfor this destination.- Platform fields, such as
privacyLevelon TikTok orboardIdon 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.
mediaKindmust be supported by every destination's platform. Atextpost cannot include Instagram, YouTube, TikTok or Pinterest.- The shared
textmust fit the text limit of every destination that does not set its owntext. To post longer text on one platform, settexton that destination. - Each
threadItemsentry 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 status | Meaning |
|---|---|
draft | Saved as a draft in the dashboard. Not published |
scheduled | Waiting for its scheduled time |
queued | Accepted and about to publish |
processing | Publishing to the platform now |
published | Live on the platform. Final unless you delete it |
failed | Not published. Final |
cancelled | Cancelled before publishing. Final |
deleted | Removed from the platform through remote delete. Final |
The post status summarizes its destinations:
| Post status | When |
|---|---|
draft, scheduled, queued, processing | At least one destination still has this status. The most advanced one wins, in the order processing, queued, scheduled, draft |
published | Every destination finished, at least one is published and none failed |
partial | Every destination finished, some published and some failed |
failed | Every destination finished, none published and at least one failed |
cancelled | Every destination finished without publishing, and at least one was cancelled |
deleted | Every 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 Createdwithstatus: "scheduled". scheduledForcan 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
faileddestination can be deleted when part of it went live, such as the first posts of a thread. - Deleting a destination that is already
deletedreturns 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:
| Field | Where | Effect |
|---|---|---|
threadItems | Request body | Publishes each item as a reply under the post, in order. 1 to 25 items, each with content and up to 20 mediaItems |
quoteTweetId | Destination | Quotes an existing post: the post id on X and Threads, an at:// URI on Bluesky |
replyToId | Destination | Publishes 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
| Code | HTTP | Cause | Fix |
|---|---|---|---|
VALIDATION_ERROR | 400 | A field is missing or invalid, or the request breaks a cross-posting rule | Correct the field named in param and the message |
CONNECTION_INACTIVE | 400 | A destination account is not active | Reconnect the account |
TOKEN_INVALID | 400 | Remote delete: the account's access was revoked | Reconnect the account, then try again |
UNSUPPORTED_PLATFORM | 400 | Remote delete is not available on this platform | Delete the post on the platform |
MEDIA_DELETE_UNSUPPORTED | 400 | Remote delete of Instagram media on an account connected with Instagram Login | Reconnect with Facebook Login, or delete the post in Instagram |
PAYMENT_METHOD_REQUIRED | 402 | The workspace needs a payment method | Add a payment method in Settings → Billing |
PAYMENT_PAST_DUE | 402 | The workspace has an unpaid invoice | Update the payment method in Settings → Billing |
INSUFFICIENT_WALLET | 402 | Remote delete on X: the wallet balance is too low | Top up the wallet in Settings → Billing |
FORBIDDEN | 403 | A read_only API key | Use a read_write key |
NOT_FOUND | 404 | The post does not exist in this workspace | Check the post id |
ACCOUNT_NOT_FOUND | 404 | A destination accountId is not a connection in this workspace | List connections and use a current id |
DESTINATION_NOT_FOUND | 404 | The destination id does not exist | Use destinations[].id from the post |
NOT_CANCELLABLE | 409 | The post is not scheduled, queued or processing, or none of its destinations can still be cancelled | Read the post. Use remote delete for published destinations |
NOT_DELETABLE | 409 | The destination has no live post on the platform | Only delete destinations that are published, or failed with an externalPostId |
RATE_LIMITED | 429 | Too many posts published to one account in a short time | Wait, then retry. See Rate Limits |
QUOTA_EXCEEDED | 429 | Remote delete: the platform's delete limit for the account is reached | Try again later. See Rate Limits |
DELETE_FAILED | 502 | The platform refused or failed the delete | Try again. If it persists, delete the post on the platform |
PLATFORM_UNAVAILABLE | 502 | The platform did not respond in time | Try 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.