Posts API

Create, schedule, and manage posts across all 12 supported platforms. The Posts API handles single posts, bulk creation, content validation, and retry logic for failed deliveries.

POST/api/v1/postsCreate or schedule a post
POST/api/v1/posts/bulkCreate multiple posts at once
GET/api/v1/postsList all posts
GET/api/v1/posts/:idGet post details
PATCH/api/v1/posts/:idUpdate a scheduled or draft post
DELETE/api/v1/posts/:idCancel or delete a post
POST/api/v1/posts/:id/retryRetry a failed post
POST/api/v1/posts/validateValidate content against platform limits

Create a post

POST/api/v1/postsCreate or schedule a post

Headers

Authorization
stringRequired

Bearer token. Example: Bearer sk_live_abc123...

Content-Type
stringRequired

Must be application/json

Idempotency-Key
stringOptional

Unique key to prevent duplicate posts. Recommended for all create operations.

Request body

content
stringOptional

Post text content. Character limits vary by platform. Optional for media-only posts.

platforms
arrayRequired

Array of platform targets. Each object must include platform and accountId. accountId may be omitted (or a bare platform string used) ONLY when exactly one account is connected on that platform — with multiple accounts the API returns a 400 listing the choices instead of silently picking one. Optionally include a content field to override the top-level content for that platform.

mediaItems
arrayOptional

Array of media attachments. Each object has type ("image" or "video") and url (a public http or https URL; local file paths and other schemes are rejected with 400 at create time — upload files first via POST /api/v1/media). Optional altText describes an image for people who use screen readers; Bluesky and Mastodon publish it with the image. If omitted and the url is your own upload, the altText stored with that upload is used.

coverImage
objectOptional

Cover image (thumbnail) for a video post: either {url} (an https link to a JPEG or PNG image) or {timestampMs} (the frame of the video to use, in milliseconds from the start). Send one of the two, and only when mediaItems includes a video. One cover applies to every platform in the post. See the Cover image callout below for what each platform does with it.

status
stringOptional

Post status.

Default: scheduled

draftscheduledimmediate
scheduledFor
stringOptional

ISO 8601 date for scheduling. Must be at least 5 minutes in the future and no more than 90 days ahead.

timezone
stringOptional

IANA timezone string (e.g. "America/New_York"). Used to interpret scheduledFor.

platformSettings
objectOptional

Platform-specific settings. Keys are platform names, values are setting objects (e.g. instagram.shareToFeed, twitter.replySettings, youtube.translate). See the YouTube translation callout below for youtube.translate.

publishNow
booleanOptional

Set to true to publish immediately, bypassing any schedule.

Default: false

queue
booleanOptional

Set to true to auto-schedule this post to the next open queue slot. Uses your dashboard Queue Settings on a standard key, or a queue created through the Queues API. Use instead of scheduledFor.

Default: false

firstComment
stringOptional

Auto-post this text as the first comment right after the post publishes (shorthand for firstComments: [{target: 0, text}]). Text and links only, no media.

firstComments
arrayOptional

Auto-posted first comments as [{target, text}]. target 0 = the main post (default); higher targets address thread entries (threads endpoint only). See the First Comment callout below for supported platforms and limits.

Per-platform content: Each object in the platforms array can include a content field that overrides the top-level content for that specific platform. This lets you tailor messaging per network while creating a single post.

Cover image (video thumbnail): Pass coverImage with a video to choose its cover. Use url for an image you made, or timestampMs to pick a frame of the video. Each platform handles it differently:

  • Instagram (Reels and single videos): uses the image or the frame. The image must be a JPEG of up to 8 MB; 9:16 works best, and other shapes are cropped to the center.
  • Facebook: Reels use the image only. Other videos use the image or the frame.
  • YouTube: uses the image only, and only on channels YouTube has verified. On other channels the video still publishes, with the thumbnail YouTube picks.
  • TikTok: uses the frame only. TikTok's API has no way to set an image cover, so a url is ignored there.
  • Pinterest: uses the image only.

Stories and carousels on Instagram don't use a cover. When you create the post, a cover on a post without a video, a link that can't be downloaded, or an image that isn't a JPEG or PNG returns 400 validation_error (sandbox keys skip the download check). The post returns it as coverImage: { url, timestampMs }; a cover set through the API has the other one null.

"mediaItems": [{ "type": "video", "url": "https://example.com/clip.mp4" }],
"coverImage": { "url": "https://example.com/cover.jpg" }

// or a frame 2.5 seconds in
"coverImage": { "timestampMs": 2500 }

First Comment (auto-plug): Pass firstComment (a plain string) or firstComments to automatically comment under the post ~15 seconds after it publishes — the classic "link in the first comment" workflow. Supported on 8 platforms: LinkedIn, Facebook, Instagram, YouTube, X (Twitter), Threads, Bluesky, and Mastodon. Not supported on TikTok, Pinterest, Telegram, or Google Business (no comment API): targeting only unsupported platforms is a validation error, while mixed selections publish the comment where supported and return a warning. Comment limits: Instagram 1,000 UTF-8 bytes, LinkedIn 1,500, X 280, Bluesky 300, Threads 500, YouTube 10,000 characters; Facebook and Mastodon have no fixed limit. Comments are text and links only. A failed comment never fails the post — read platforms[].firstComments on GET /api/v1/posts/:id for per-account delivery status (posted with the comment URL, failed, or skipped).

Queue integration: Instead of setting scheduledFor manually, pass "queue": true to automatically schedule the post into the next open slot from your dashboard Queue Settings (Settings → Queue Settings). See the Queues API for how the slot is picked.

TikTok requires a privacy level. When you target tiktok, you must include platformSettings.tiktok.privacy. Valid values are PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, and SELF_ONLY, and the value must be enabled on the connected account. Posts are rejected at creation rather than published with a guessed privacy level.

Mastodon options live in platformSettings.mastodon. Set visibility to public (default), unlisted, private, or direct. Also supported: spoilerText (content warning), sensitive (boolean, blurs media), and language (ISO 639-1). See the Mastodon platform guide for polls and full details.

YouTube translation. Set platformSettings.youtube.translate to true to translate the title and description into 10 languages (Korean, Japanese, Spanish, Portuguese, German, French, Russian, Hindi, Indonesian, Chinese) when the video uploads, so viewers whose YouTube is set to one of those languages see the title in their language, Shorts included. false turns it off for that post, and a value that is not a boolean returns 400 validation_error. If you leave it out, API keys (including the CLI and the npm MCP package) and partner keys do not translate, while your own Claude or ChatGPT connector follows the "Translate title and description" switch in your composer. Partner apps must get each end user's consent before sending true, because the YouTube API Services Developer Policies require consent before changing upload metadata. After publishing, platforms[].localizedLanguages on GET /api/v1/posts/:id lists the language codes stored on the video ([] means translation was on but none were stored). On the Claude and ChatGPT connectors, a video gets only translations the user has seen: show_post_preview shows them, and a post with the same YouTube title and description publishes exactly those. Before upload, platforms[].translation reads previewed (with the language codes) or not_previewed (a connector post that never showed them, so none are added).

"platformSettings": {
  "youtube": { "title": "My new video", "translate": true }
}

Your own translations. Send platformSettings.youtube.localizations to set the title and description yourself for any language YouTube supports, keyed by language code (ko, ja, es, pt-PT, zh-TW and so on; common spellings like he, nb, zh-Hans and pt-BR are mapped to YouTube's). Each entry needs a title of up to 100 characters and can have a description of up to 5000, counted the way YouTube counts them (an emoji is one character) after the clean-up every upload gets (curly quotes become straight, "…" becomes "...", < and > are removed). Without a description, our translated description for that language is used, or your main description. With translate: true we translate the other languages and yours win where both exist; without it only your languages are added. They publish as written, except an entry for the video's main language, which YouTube replaces with the main title (the main language is English unless translation detects another). An invalid entry returns 400 validation_error naming the language, so a bad translation never fails the upload. On PATCH, a new map replaces the old one and null removes it; changing only the title or description keeps your translations, so send new ones when they no longer fit. Before upload, platforms[].translation.ownLanguages lists them.

"platformSettings": {
  "youtube": {
    "title": "My new video",
    "translate": true,
    "localizations": {
      "ko": { "title": "내 새 영상", "description": "한국어 설명" },
      "pt-PT": { "title": "O meu novo vídeo" }
    }
  }
}
# Publish immediately
schedulala post "Excited to announce our new feature!" --platforms twitter,linkedin,instagram --now
# Schedule for later
schedulala post "Excited to announce our new feature!" \
--platforms twitter,linkedin,instagram \
--schedule "2026-03-15T14:00:00Z"
# With media
schedulala post "Check it out" --platforms instagram \
--media "https://example.com/announcement.jpg" --now
# Platform-specific content
schedulala post "Default text" --platforms twitter,linkedin \
--platform-content '{"linkedin":{"content":"LinkedIn version"}}' --now
{
"success": true,
"post": {
"id": "67abc456def789012345cdef",
"status": "scheduled",
"content": "Excited to announce our new feature! Check it out at example.com",
"scheduledFor": "2026-03-15T14:00:00.000Z",
"platforms": [
{
"platform": "twitter",
"accountId": "67abc123def4567890120001",
"status": "pending"
},
{
"platform": "linkedin",
"accountId": "67abc123def4567890120002",
"status": "pending"
},
{
"platform": "instagram",
"accountId": "67abc123def4567890120003",
"status": "pending"
}
],
"createdAt": "2026-03-01T12:00:00.000Z"
}
}

Bulk create posts

POST/api/v1/posts/bulkCreate multiple posts at once

Create up to 25 posts in a single request. Each post in the array follows the same schema as the create post endpoint. The response includes a summary with per-post results so you can identify any failures.

Request body

posts
arrayRequired

Array of post objects (max 25). Each follows the same schema as POST /api/v1/posts.

timezone
stringOptional

Default timezone for all posts. Individual posts can override this.

curl -X POST https://schedulala.com/api/v1/posts/bulk \
-H "Authorization: Bearer sk_live_abc123..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: bulk_$(uuidgen)" \
-d '{
"timezone": "America/New_York",
"posts": [
{
"content": "Monday motivation! Start the week strong.",
"platforms": [{ "platform": "twitter", "accountId": "67abc123def4567890120001" }],
"scheduledFor": "2026-03-16T09:00:00Z"
},
{
"content": "Wednesday tip: consistency beats perfection.",
"platforms": [{ "platform": "twitter", "accountId": "67abc123def4567890120001" }],
"scheduledFor": "2026-03-18T09:00:00Z"
}
]
}'
{
"success": true,
"results": [
{
"index": 0,
"success": true,
"post": {
"id": "67abc456def789012345cd01",
"status": "scheduled",
"content": "Monday motivation! Start the week strong.",
"scheduledFor": "2026-03-16T09:00:00.000Z",
"platforms": [
{
"platform": "twitter",
"accountId": "67abc123def4567890120001",
"status": "pending"
}
],
"createdAt": "2026-03-01T12:00:00.000Z"
}
},
{
"index": 1,
"success": true,
"post": {
"id": "67abc456def789012345cd02",
"status": "scheduled",
"content": "Wednesday tip: consistency beats perfection.",
"scheduledFor": "2026-03-18T09:00:00.000Z",
"platforms": [
{
"platform": "twitter",
"accountId": "67abc123def4567890120001",
"status": "pending"
}
],
"createdAt": "2026-03-01T12:00:00.000Z"
}
}
],
"summary": {
"total": 2,
"succeeded": 2,
"failed": 0
}
}

List posts

GET/api/v1/postsList all posts

Retrieve a paginated list of your posts. Supports filtering by status, platform, profile, and date range. Results are returned in reverse chronological order.

Query parameters

status
stringOptional

Filter by post status.

draftscheduledimmediatepostedpartially_failedfailedcancelled
brandId
stringOptional

Filter by brand ID.

platform
stringOptional

Filter by platform name.

twitterinstagramfacebooklinkedintiktokthreadsblueskyyoutubepinteresttelegram
limit
numberOptional

Number of posts to return.

Default: 20

page
numberOptional

Page number for offset-based pagination.

Default: 1

after
stringOptional

Return posts scheduled after this ISO 8601 date.

before
stringOptional

Return posts scheduled before this ISO 8601 date.

# List all posts
schedulala post:list
# Filter by status
schedulala post:list --status scheduled --limit 10
{
"success": true,
"posts": [
{
"id": "67abc456def789012345cdef",
"status": "scheduled",
"content": "Excited to announce our new feature!",
"scheduledFor": "2026-03-15T14:00:00.000Z",
"platforms": [
{
"platform": "twitter",
"accountId": "67abc123def4567890120001",
"status": "pending"
}
],
"createdAt": "2026-03-01T12:00:00.000Z"
}
],
"pagination": {
"total": 45,
"page": 1,
"limit": 10,
"pages": 5,
"hasMore": true
}
}

Get post details

GET/api/v1/posts/:idGet post details

Retrieve full details for a single post, including per-platform delivery status. After a post is published, each platform entry includes the native postId, postUrl, and postedAt timestamp.

Path parameters

id
stringRequired

The post ID (e.g. 67abc456def789012345cdef).

schedulala post:status 67abc456def789012345cdef
# Watch until published or failed
schedulala post:status 67abc456def789012345cdef --watch
{
"success": true,
"post": {
"id": "67abc456def789012345cdef",
"status": "posted",
"content": "Excited to announce our new feature! Check it out at example.com",
"scheduledFor": "2026-03-15T14:00:00.000Z",
"platforms": [
{
"platform": "twitter",
"accountId": "67abc123def4567890120001",
"status": "posted",
"postId": "1897654321098765432",
"postUrl": "https://x.com/yourhandle/status/1897654321098765432",
"postedAt": "2026-03-15T14:00:05.000Z"
},
{
"platform": "linkedin",
"accountId": "67abc123def4567890120002",
"status": "posted",
"postId": "urn:li:share:7654321098765432100",
"postUrl": "https://www.linkedin.com/feed/update/urn:li:share:7654321098765432100",
"postedAt": "2026-03-15T14:00:08.000Z",
"firstComments": [
{
"target": 0,
"status": "posted",
"commentId": "urn:li:comment:(urn:li:share:7654321098765432100,7654321098765432101)",
"url": "https://www.linkedin.com/feed/update/urn:li:share:7654321098765432100",
"postedAt": "2026-03-15T14:00:24.000Z"
}
]
},
{
"platform": "instagram",
"accountId": "67abc123def4567890120003",
"status": "posted",
"postId": "17890012345678901",
"postUrl": "https://www.instagram.com/p/ABC123xyz/",
"postedAt": "2026-03-15T14:00:12.000Z"
}
],
"mediaItems": [
{
"type": "image",
"url": "https://example.com/announcement.jpg"
}
],
"firstComments": [
{
"target": 0,
"text": "Full story: https://example.com/blog/new-feature"
}
],
"createdAt": "2026-03-01T12:00:00.000Z",
"updatedAt": "2026-03-15T14:00:12.000Z"
}
}

Update a post

PATCH/api/v1/posts/:idUpdate a scheduled or draft post

Update a post that is in draft or scheduled status. Published, failed, and cancelled posts cannot be updated. Only include the fields you want to change — omitted fields are left unchanged. When you change a post's YouTube content, the video's other YouTube settings (privacy, tags, category, made-for-kids, and translate) are kept.

Path parameters

id
stringRequired

The post ID to update.

Request body

content
stringOptional

Updated post text content.

platforms
arrayOptional

Updated platform targets.

mediaItems
arrayOptional

Updated media attachments (replaces all media). Same shape as on create, including optional altText.

scheduledFor
stringOptional

Updated schedule time (ISO 8601).

timezone
stringOptional

Updated timezone.

platformSettings
objectOptional

Updated platform-specific settings.

status
stringOptional

Change status (e.g. move a draft to scheduled).

draftscheduled
firstComment
stringOptional

Replace the auto-posted first comment on the main post (shorthand for firstComments: [{target: 0, text}]).

firstComments
arrayOptional

Replace ALL auto-posted first comments ([{target, text}]). Pass an empty array to clear pending comments — delivery reads the post at publish time, so changes apply without rescheduling.

curl -X PATCH https://schedulala.com/api/v1/posts/67abc456def789012345cdef \
-H "Authorization: Bearer sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"content": "Updated announcement! Check out our new feature at example.com",
"scheduledFor": "2026-03-16T10:00:00Z"
}'
{
"success": true,
"post": {
"id": "67abc456def789012345cdef",
"status": "scheduled",
"content": "Updated announcement! Check out our new feature at example.com",
"scheduledFor": "2026-03-16T10:00:00.000Z",
"platforms": [
{
"platform": "twitter",
"accountId": "67abc123def4567890120001",
"status": "pending"
},
{
"platform": "linkedin",
"accountId": "67abc123def4567890120002",
"status": "pending"
},
{
"platform": "instagram",
"accountId": "67abc123def4567890120003",
"status": "pending"
}
],
"createdAt": "2026-03-01T12:00:00.000Z",
"updatedAt": "2026-03-01T13:30:00.000Z"
}
}

Delete a post

DELETE/api/v1/posts/:idCancel or delete a post

Cancel a scheduled or draft post. The post status is set to cancelled and it will not be published. Already-published posts cannot be deleted through this endpoint.

Path parameters

id
stringRequired

The post ID to cancel.

schedulala post:delete 67abc456def789012345cdef
{
"success": true,
"deleted": true,
"id": "67abc456def789012345cdef",
"method": "cancelled"
}

Retry a failed post

POST/api/v1/posts/:id/retryRetry a failed post

Retry delivery for a post with status failed or partially_failed. You can optionally specify which platforms to retry and add a delay before the retry attempt.

Path parameters

id
stringRequired

The post ID to retry.

Request body

platforms
arrayOptional

Array of platform names to retry. If omitted, all failed platforms are retried.

delay
numberOptional

Seconds to wait before retrying.

Default: 0

schedulala post:retry 67abc456def789012345cdef
{
"success": true,
"post": {
"id": "67abc456def789012345cdef",
"status": "scheduled",
"content": "Excited to announce our new feature! Check it out at example.com",
"platforms": [
{
"platform": "twitter",
"accountId": "67abc123def4567890120001",
"status": "posted",
"postId": "1897654321098765432",
"postUrl": "https://x.com/yourhandle/status/1897654321098765432",
"postedAt": "2026-03-15T14:00:05.000Z"
},
{
"platform": "instagram",
"accountId": "67abc123def4567890120003",
"status": "pending",
"previousError": "Media upload timeout"
}
]
},
"retriedPlatforms": [
"instagram"
]
}

Validate content

POST/api/v1/posts/validateValidate content against platform limits

Check whether your content meets each platform's requirements before creating a post. Returns per-platform validation results including character counts, media compatibility, and any errors.

Request body

content
stringRequired

The text content to validate.

platforms
arrayRequired

Array of platform name strings to validate against (e.g. ["twitter", "instagram", "tiktok"]).

mediaItems
arrayOptional

Array of media items to validate. Each has type and url.

coverImage
objectOptional

Cover image to check, same shape as on create ({url} or {timestampMs}). A problem with it answers 200 with validation.isValid false, the message added to each platform's errors, and the details under coverImage, the same way an unreachable media link is reported here. POST /api/v1/posts returns 400 validation_error for the same problem.

firstComment
stringOptional

First-comment text to validate against each platform's comment limits (shorthand for firstComments: [{target: 0, text}]).

firstComments
arrayOptional

First comments to validate ([{target, text}]) — reports the same errors and warnings POST /api/v1/posts would return.

curl -X POST https://schedulala.com/api/v1/posts/validate \
-H "Authorization: Bearer sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"content": "Check out our latest product update! We have been working hard to bring you the best experience possible.",
"platforms": ["twitter", "tiktok", "instagram"],
"mediaItems": [
{ "type": "image", "url": "https://example.com/product.jpg" }
]
}'
{
"success": true,
"validation": {
"isValid": false,
"platforms": {
"twitter": {
"valid": true,
"contentLength": 42,
"maxLength": 280,
"remainingCharacters": 238,
"mediaValid": true
},
"tiktok": {
"valid": false,
"errors": [
"TikTok requires a video. Image-only posts are not supported."
],
"mediaValid": false
},
"instagram": {
"valid": true,
"contentLength": 42,
"maxLength": 2200,
"remainingCharacters": 2158,
"mediaValid": true
}
}
}
}

Related