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.
/api/v1/postsCreate or schedule a post/api/v1/posts/bulkCreate multiple posts at once/api/v1/postsList all posts/api/v1/posts/:idGet post details/api/v1/posts/:idUpdate a scheduled or draft post/api/v1/posts/:idCancel or delete a post/api/v1/posts/:id/retryRetry a failed post/api/v1/posts/validateValidate content against platform limitsCreate a post
/api/v1/postsCreate or schedule a postHeaders
| Name | Type | Required | Description |
|---|---|---|---|
Authorization | string | Required | Bearer token. Example: Bearer sk_live_abc123... |
Content-Type | string | Required | Must be application/json |
Idempotency-Key | string | Optional | Unique key to prevent duplicate posts. Recommended for all create operations. |
AuthorizationstringRequiredBearer token. Example: Bearer sk_live_abc123...
Content-TypestringRequiredMust be application/json
Idempotency-KeystringOptionalUnique key to prevent duplicate posts. Recommended for all create operations.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
content | string | Optional | Post text content. Character limits vary by platform. Optional for media-only posts. |
platforms | array | Required | 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 | array | Optional | 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 | object | Optional | 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 | string | Optional | Post status.Default: scheduleddraftscheduledimmediate |
scheduledFor | string | Optional | ISO 8601 date for scheduling. Must be at least 5 minutes in the future and no more than 90 days ahead. |
timezone | string | Optional | IANA timezone string (e.g. "America/New_York"). Used to interpret scheduledFor. |
platformSettings | object | Optional | 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 | boolean | Optional | Set to true to publish immediately, bypassing any schedule.Default: false |
queue | boolean | Optional | 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 | string | Optional | 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 | array | Optional | 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. |
contentstringOptionalPost text content. Character limits vary by platform. Optional for media-only posts.
platformsarrayRequiredArray 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.
mediaItemsarrayOptionalArray 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.
coverImageobjectOptionalCover 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.
statusstringOptionalPost status.
Default: scheduled
draftscheduledimmediatescheduledForstringOptionalISO 8601 date for scheduling. Must be at least 5 minutes in the future and no more than 90 days ahead.
timezonestringOptionalIANA timezone string (e.g. "America/New_York"). Used to interpret scheduledFor.
platformSettingsobjectOptionalPlatform-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.
publishNowbooleanOptionalSet to true to publish immediately, bypassing any schedule.
Default: false
queuebooleanOptionalSet 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
firstCommentstringOptionalAuto-post this text as the first comment right after the post publishes (shorthand for firstComments: [{target: 0, text}]). Text and links only, no media.
firstCommentsarrayOptionalAuto-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 immediatelyschedulala post "Excited to announce our new feature!" --platforms twitter,linkedin,instagram --now# Schedule for laterschedulala post "Excited to announce our new feature!" \--platforms twitter,linkedin,instagram \--schedule "2026-03-15T14:00:00Z"# With mediaschedulala post "Check it out" --platforms instagram \--media "https://example.com/announcement.jpg" --now# Platform-specific contentschedulala 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
/api/v1/posts/bulkCreate multiple posts at onceCreate 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
| Name | Type | Required | Description |
|---|---|---|---|
posts | array | Required | Array of post objects (max 25). Each follows the same schema as POST /api/v1/posts. |
timezone | string | Optional | Default timezone for all posts. Individual posts can override this. |
postsarrayRequiredArray of post objects (max 25). Each follows the same schema as POST /api/v1/posts.
timezonestringOptionalDefault 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
/api/v1/postsList all postsRetrieve a paginated list of your posts. Supports filtering by status, platform, profile, and date range. Results are returned in reverse chronological order.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
status | string | Optional | Filter by post status.draftscheduledimmediatepostedpartially_failedfailedcancelled |
brandId | string | Optional | Filter by brand ID. |
platform | string | Optional | Filter by platform name.twitterinstagramfacebooklinkedintiktokthreadsblueskyyoutubepinteresttelegram |
limit | number | Optional | Number of posts to return.Default: 20 |
page | number | Optional | Page number for offset-based pagination.Default: 1 |
after | string | Optional | Return posts scheduled after this ISO 8601 date. |
before | string | Optional | Return posts scheduled before this ISO 8601 date. |
statusstringOptionalFilter by post status.
draftscheduledimmediatepostedpartially_failedfailedcancelledbrandIdstringOptionalFilter by brand ID.
platformstringOptionalFilter by platform name.
twitterinstagramfacebooklinkedintiktokthreadsblueskyyoutubepinteresttelegramlimitnumberOptionalNumber of posts to return.
Default: 20
pagenumberOptionalPage number for offset-based pagination.
Default: 1
afterstringOptionalReturn posts scheduled after this ISO 8601 date.
beforestringOptionalReturn posts scheduled before this ISO 8601 date.
# List all postsschedulala post:list# Filter by statusschedulala 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
/api/v1/posts/:idGet post detailsRetrieve 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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Required | The post ID (e.g. 67abc456def789012345cdef). |
idstringRequiredThe post ID (e.g. 67abc456def789012345cdef).
schedulala post:status 67abc456def789012345cdef# Watch until published or failedschedulala 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
/api/v1/posts/:idUpdate a scheduled or draft postUpdate 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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Required | The post ID to update. |
idstringRequiredThe post ID to update.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
content | string | Optional | Updated post text content. |
platforms | array | Optional | Updated platform targets. |
mediaItems | array | Optional | Updated media attachments (replaces all media). Same shape as on create, including optional altText. |
scheduledFor | string | Optional | Updated schedule time (ISO 8601). |
timezone | string | Optional | Updated timezone. |
platformSettings | object | Optional | Updated platform-specific settings. |
status | string | Optional | Change status (e.g. move a draft to scheduled).draftscheduled |
firstComment | string | Optional | Replace the auto-posted first comment on the main post (shorthand for firstComments: [{target: 0, text}]). |
firstComments | array | Optional | 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. |
contentstringOptionalUpdated post text content.
platformsarrayOptionalUpdated platform targets.
mediaItemsarrayOptionalUpdated media attachments (replaces all media). Same shape as on create, including optional altText.
scheduledForstringOptionalUpdated schedule time (ISO 8601).
timezonestringOptionalUpdated timezone.
platformSettingsobjectOptionalUpdated platform-specific settings.
statusstringOptionalChange status (e.g. move a draft to scheduled).
draftscheduledfirstCommentstringOptionalReplace the auto-posted first comment on the main post (shorthand for firstComments: [{target: 0, text}]).
firstCommentsarrayOptionalReplace 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
/api/v1/posts/:idCancel or delete a postCancel 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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Required | The post ID to cancel. |
idstringRequiredThe post ID to cancel.
schedulala post:delete 67abc456def789012345cdef
{"success": true,"deleted": true,"id": "67abc456def789012345cdef","method": "cancelled"}
Retry a failed post
/api/v1/posts/:id/retryRetry a failed postRetry 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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Required | The post ID to retry. |
idstringRequiredThe post ID to retry.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
platforms | array | Optional | Array of platform names to retry. If omitted, all failed platforms are retried. |
delay | number | Optional | Seconds to wait before retrying.Default: 0 |
platformsarrayOptionalArray of platform names to retry. If omitted, all failed platforms are retried.
delaynumberOptionalSeconds 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
/api/v1/posts/validateValidate content against platform limitsCheck 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
| Name | Type | Required | Description |
|---|---|---|---|
content | string | Required | The text content to validate. |
platforms | array | Required | Array of platform name strings to validate against (e.g. ["twitter", "instagram", "tiktok"]). |
mediaItems | array | Optional | Array of media items to validate. Each has type and url. |
coverImage | object | Optional | 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 | string | Optional | First-comment text to validate against each platform's comment limits (shorthand for firstComments: [{target: 0, text}]). |
firstComments | array | Optional | First comments to validate ([{target, text}]) — reports the same errors and warnings POST /api/v1/posts would return. |
contentstringRequiredThe text content to validate.
platformsarrayRequiredArray of platform name strings to validate against (e.g. ["twitter", "instagram", "tiktok"]).
mediaItemsarrayOptionalArray of media items to validate. Each has type and url.
coverImageobjectOptionalCover 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.
firstCommentstringOptionalFirst-comment text to validate against each platform's comment limits (shorthand for firstComments: [{target: 0, text}]).
firstCommentsarrayOptionalFirst 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}}}}