Posts
Create a post and read its status. Drafts by default; pass scheduled_for to schedule it for real. See the policy note.
Create a post
/api/v1/postsCreates one post targeting one or more channels. Each channel can use the top-level caption verbatim, or provide its own override. YouTube channels accept title and description in addition, and channels[].options carries the full set of per-platform publish options the dashboard composer offers (YouTube privacy, category, tags and disclosures; Instagram story / feed / collaborators; Facebook story / video title; TikTok audience and compliance toggles; LinkedIn visibility and reshare control; image alt text on Meta and LinkedIn).
Media is optional where the platform allows it: text-only posts publish fine to Facebook, LinkedIn, and webhooks, while Instagram, TikTok, and YouTube need media. Check each channel's accepts block from GET /channels before composing.
Request
POST /api/v1/posts HTTP/1.1
Host: postme.live
Authorization: Bearer pml_live_AbCdEf123456_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Idempotency-Key: 6f4b1c8e-3e5e-4b9c-9b4f-0e0e3c1f9a2c
{
"caption": "We just shipped v0.20!",
"channels": [
{ "id": "5a1d…", "caption": "Big day! 🚀 #shipping",
"options": { "share_to_story": true, "collaborators": ["teammate"] } },
{ "id": "6b2e…", "title": "What we shipped in v0.20", "description": "Long-form notes…",
"options": { "privacy_status": "unlisted", "tags": ["changelog", "shipping"] } }
],
"media": [
{ "id": "9f3a…" },
{ "url": "https://example.com/hero.jpg" }
]
}Field reference
| Field | Type | Notes |
|---|---|---|
caption | string, 1–5000 | Default caption for every channel that doesn't override. |
channels[] | array, 1–25 | Each item targets one connected account. |
channels[].id | uuid | From GET /channels. |
channels[].caption | string, optional | Per-channel override. Falls back to top-level caption. |
channels[].title | string, optional | YouTube-only. A publishing YouTube target needs a title or a caption to derive one from; drafts may omit it. |
channels[].description | string, optional | YouTube description; ignored on other platforms. |
channels[].options | object, optional | Per-platform publish options; see the table below. Unknown keys are a 400; known keys on the wrong platform are a 422. |
media[] | array, 0–20 | Either { id } (pre-uploaded via POST /media) or { url } (server fetches; HTTPS only). |
scheduled_for | ISO 8601, optional | Schedules the post instead of drafting it. See Scheduling below. |
publish_now | boolean, optional | Publishes immediately instead of drafting. See Publish now below. Mutually exclusive with scheduled_for. |
Scheduling
Pass scheduled_for (ISO 8601, at least 2 minutes in the future) and the post is created with status: queued and publishes automatically at that time. It's the same delayed job the dashboard composer uses, so you can still cancel it from the dashboard ("Cancel scheduled"). Omit the field for the classic draft-only behavior.
{
"caption": "Going live Friday 9am 🎙️",
"scheduled_for": "2026-06-19T23:00:00Z",
"channels": [ { "id": "5a1d…" } ],
"media": [ { "id": "9f3a…" } ]
}- Content rules are per-platform (
422when a target can't publish the shape): text-only posts are fine on Facebook, LinkedIn, and webhooks; Instagram, TikTok, and YouTube targets need media. Drafts stay lenient while being assembled. - The scheduled path consumes one post-quota slot and respects the pending-schedule cap: breaches return
402 quota_exceededwithused / limit / reset_at / upgrade_urlinerror.details. - To keep something draft-only, omit both
scheduled_forandpublish_nowwhen creating. You can publish it later by patching either field in — seePATCH /posts/{id}.
Publish now
Pass publish_now: true and the post is created with status: queued and handed straight to the worker, with no delay and no review step. It's the immediate counterpart to scheduling.
{
"caption": "Live now 🎙️",
"publish_now": true,
"channels": [ { "id": "5a1d…" } ],
"media": [ { "id": "9f3a…" } ]
}- Mutually exclusive with
scheduled_for. Sending both is a422 unprocessable. - The same per-platform content rules as scheduling apply: text-only is fine on Facebook, LinkedIn, and webhooks; Instagram/TikTok/YouTube need media (
422otherwise). - Consumes one post-quota slot and respects the same caps; breaches return
402 quota_exceededwithused / limit / reset_at / upgrade_urlinerror.details. - The create response returns
status: queuedimmediately; pollGET /posts/{id}for per-channel publish status.
Per-platform options
Supported on youtube, meta_instagram, meta_facebook, tiktok, linkedin_personal and linkedin_company channels — the same settings the dashboard composer exposes. custom_webhook has no options. TikTok and LinkedIn currently publish to existing connections only: current plans don't offer new connections to them, so their options apply to channels connected earlier.
| Platform | Option | Type | Notes |
|---|---|---|---|
| YouTube | privacy_status | public | unlisted | private | Defaults to public. |
| YouTube | category_id | string | Numeric category id ("22" = People & Blogs). |
| YouTube | tags | string[], ≤50 | Combined length ≤ 500 characters. |
| YouTube | made_for_kids | boolean | COPPA self-declaration. |
| YouTube | contains_synthetic_media | boolean | AI / synthetic-media disclosure. |
| Facebook + Instagram | share_to_story | boolean | Also share the media to Stories after the post/reel publishes. Stories carry no caption or link (Meta API limit); best-effort, so a story failure never fails the post. |
| Facebook, Instagram + LinkedIn | image_alt_texts | string[], ≤20 | One alt text per attached image, by media order. ≤1000 chars each. |
share_to_feed | boolean | Reels only. false keeps the reel out of the main feed/grid. | |
collaborators | string[], ≤3 | Usernames invited as collaborators; they must accept the invite. | |
video_title | string, ≤255 | Title on video posts. | |
| TikTok | privacy_level | PUBLIC_TO_EVERYONE | MUTUAL_FOLLOW_FRIENDS | FOLLOWER_OF_CREATOR | SELF_ONLY | Required when publishing to a direct-post channel (accepts.direct_post: true) — TikTok forbids a preselected default. Inbox-mode channels ignore it. |
| TikTok | disable_comment / disable_duet / disable_stitch | boolean | Direct post only. Turn off comments, Duets, or Stitches. |
| TikTok | commercial_content | boolean | Direct post only. When true, at least one of brand_organic_toggle / brand_content_toggle must be set (422 otherwise). |
| TikTok | brand_organic_toggle / brand_content_toggle | boolean | Your-brand / branded-content disclosures. Branded content can't combine with privacy_level: SELF_ONLY. |
| TikTok | is_aigc | boolean | AI-generated-content disclosure. |
visibility | PUBLIC | CONNECTIONS | Who can see the post; defaults to PUBLIC. | |
reshare_disabled | boolean | Block reposts of this post. |
Response: 202 Accepted
{
"id": "7d0a…",
"status": "draft",
"caption": "We just shipped v0.20!",
"channels": [
{ "id": "1…", "channel_id": "5a1d…", "platform": "meta_instagram", "status": "pending",
"external_post_id": null, "external_post_url": null, "error_message": null,
"options": { "share_to_story": true, "collaborators": ["teammate"] } },
{ "id": "2…", "channel_id": "6b2e…", "platform": "youtube", "status": "pending",
"external_post_id": null, "external_post_url": null, "error_message": null,
"options": { "privacy_status": "unlisted", "tags": ["changelog", "shipping"] } }
],
"media_ids": ["9f3a…", "ab12…"],
"scheduled_for": null,
"review_url": "https://postme.live/app/post?draft=7d0a…",
"created_at": "2026-05-15T08:11:19Z",
"updated_at": "2026-05-15T08:11:19Z",
"meta": {
"note": "v1 API: post persisted as draft. Review and publish from the dashboard."
}
}List posts
/api/v1/postsLists the workspace's posts (drafts, scheduled, and published) newest first, cursor-paginated. Each item is the same shape as the create response. Scope posts:read.
curl "https://postme.live/api/v1/posts?status=queued&scheduled=true&limit=50" \
-H "Authorization: Bearer pml_live_..."Filters
All optional; combine freely. They're AND-ed together and applied on top of cursor paging.
| Parameter | Type | Notes |
|---|---|---|
status | enum | One of draft, queued, posting, partial, posted, failed. Anything else is a 400. |
scheduled | true | Restricts to pending scheduled posts only (a future scheduled_for that is still queued). |
channel_id | uuid | Only posts targeting this channel. |
before / after | ISO 8601 | Bound by creation time: created_at strictly before / after the given instant. A malformed value is a 400. |
Returns the standard { data, has_more, next_cursor } envelope.
Get a post
/api/v1/posts/{id}Returns the same shape as the create response, refreshed from the database.
curl https://postme.live/api/v1/posts/7d0a… \
-H "Authorization: Bearer pml_live_..."Returns 404 not_found when the post does not exist or belongs to a different organization than the API key.
Edit a draft
/api/v1/posts/{id}Edit a draft in place. Every field is optional and replace-on-present: a field you omit stays as it was; a field you send overwrites it. Send at least one. Scope posts:write; requires an Idempotency-Key like every write.
captionreplaces the base caption (and propagates to any channel that didn't set its own override).channelsreplaces the whole target set, including per-channel captions andoptions. Omit it to keep the current targets; it can't be sent empty.mediareplaces attachments.[]clears them; omittingmediakeeps them.
# Revise a draft's copy and swap its image
curl -X PATCH https://postme.live/api/v1/posts/7d0a8b1c-2e3f-4a5b-9c0d-1e2f3a4b5c6d \
-H "Authorization: Bearer pml_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"caption": "Tightened headline ✨",
"media": [{ "id": "9f3a…" }]
}'Publish a draft
Add scheduled_for (ISO 8601, ≥ 2 minutes ahead) or publish_now: true to the same call to publish the draft you created earlier — this is the only way to publish an existing draft. It behaves exactly like POST /posts: mutually exclusive, consumes one post-quota slot (402 on breach), and enforces the per-platform content rules (422 on an unpublishable combination).
# Schedule the same draft to go out later
curl -X PATCH https://postme.live/api/v1/posts/7d0a8b1c-2e3f-4a5b-9c0d-1e2f3a4b5c6d \
-H "Authorization: Bearer pml_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "scheduled_for": "2026-07-06T15:00:00Z" }'Returns 200 OK with the updated post (same shape as create). Only composed drafts are editable: a post that is already scheduled, publishing, published, or failed returns 409 — unschedule a scheduled post first with DELETE /posts/{id}. discovered posts (source: "discovered") are read-only and also return 409. 404 when the post isn't yours.
Delete a draft, or unschedule a scheduled post
/api/v1/posts/{id}Exactly two outcomes, reported by action in the response: a draft is permanently deleted, and a queued (scheduled or publishing-soon) post is unscheduled — its pending publish is cancelled and the post reverts to a draft, same as "Cancel scheduled" in the dashboard. The composed content survives as a draft. Everything else returns 409: posting/partial/posted are immutable publish history, failed posts keep their per-channel error record, and discovered posts mirror content published outside postme. Published and discovered posts can never be deleted — this endpoint never removes content from the social platforms. Scope posts:write; like every write it requires an Idempotency-Key.
curl -X DELETE https://postme.live/api/v1/posts/7d0a8b1c-2e3f-4a5b-9c0d-1e2f3a4b5c6d \
-H "Authorization: Bearer pml_live_..." \
-H "Idempotency-Key: $(uuidgen)"Response: 200 OK
{
"id": "7d0a8b1c-2e3f-4a5b-9c0d-1e2f3a4b5c6d",
"action": "unscheduled",
"deleted": false,
"previous_status": "queued"
}action: deleted— the draft row is gone.action: unscheduled— the post is back tostatus: draft; edit and reschedule it any time.- Unscheduling gives no quota refund; the consumed post slot stays spent (same as the dashboard).
404 not_foundwhen the post doesn't exist or isn't yours;409 conflictwhen it is publishing, published, failed, or adiscoveredpost.