Create a post
Create a post; action decides where it lands. draft saves it (prechecks only warn); schedule queues it (monthly quota + hard prechecks); publish sends it now (hard prechecks + rate caps; credentials that require confirmation get a draft plus a confirmToken, and only confirm actually publishes). Publishing is asynchronous: after status=publishing, poll the post for per-account results.
/api/v1/postsAuthorization & guardrails
- Send
Authorization: Bearer <credential>; credentials are issued under Settings → Integrations → Long-lived credentials, see Authentication. - The credential needs scope
posts:write/schedule:write/publish:execute(any one lets you call it; which tier you may use is decided by the credential's publish allow-list); the issuer's workspace role must also allow the action. - Rate limit: write tier 10/min; with
action: "publish"the publish tier applies, 5/min (per credential). - Idempotency: optional for draft / schedule; required with
action: "publish".
Request body (application/json)
actionenum: draft · schedule · publishrequiredWhere the post lands: draft; schedule (needs scheduledAt); publish immediately.
contentstringrequiredMain copy; may be an empty string when every target platform has its own copy in variants.
platformsstring[] (enum: x · youtube · instagram · tiktok · facebook · threads · douyin · xiaohongshu · wechat_mp)requiredTarget platforms; may be empty for drafts.
titlestringoptionalTitle (required for YouTube).
targetAccountIdsstring[]optionalAccount ids (from GET /v1/accounts). Omit = every usable account on those platforms (expired ones skipped), at most 20 per post. When given, only these accounts, and each must belong to a listed platform.
scheduledAtdatetime (ISO 8601, with timezone)optionalRequired for schedule; must carry a timezone (Z or ±HH:MM) and be at least 60 s in the future; on a draft it is only the planned date on the calendar.
mediaobject[] { kind, dataUrl, platform?, durationSec? }optionalkind is image / cover / video; dataUrl must be a popkit storage URL (get one via POST /v1/media), image data URLs are accepted for images / covers; platform marks a per-platform override.
variantsobject[] { platform, content?, title? }optionalPer-platform copy overrides; send only the platforms that differ.
tiktokOptionsobjectoptionalTikTok Direct Post compliance options: privacyLevel (must be chosen by a person when posting to TikTok), allowComment, allowDuet, allowStitch, commercial, yourBrand, brandedContent, coverTimestampMs.
youtubeOptionsobject { categoryId?, madeForKids?, playlistId? }optionalYouTube category, made-for-kids flag, and the id of a playlist to add the video to after publishing (must belong to the target channel; system lists such as Watch Later are unavailable; a failed add does not fail the publish).
threadsOptionsobject { replyControl?, topicTag?, linkAttachment?, replyApprovals? }optionalThreads options: replyControl — who can reply (everyone / followers_only / accounts_you_follow / mentioned_only / parent_post_author_only; default everyone); topicTag — topic tag (1–50 characters, no '.' or '&'); linkAttachment — link card (an http(s) URL; text-only posts only; counts toward the 5-link limit); replyApprovals — reply approvals (true = replies from others stay hidden until the author approves them; default off).
clientRequestIdstringoptionalIdempotency key (8–128 chars of [A-Za-z0-9_.:-]); prefer the Idempotency-Key header, which wins over the body. Required for the publish tier.
Rules
- Idempotency-Key is required for the publish tier and honoured for draft / schedule.
- Retrying with the same key never creates twice; the response carries meta.replayed: true.
- TikTok posts need a privacyLevel chosen by a person; YouTube posts need a title.
Responses
Success is HTTP 201 with a { data, meta } envelope; examples per status are on the right (at the end of the page on narrow screens). Every error is { error: { code, message, params }, meta } — branch on the code, not the message; see Errors.
| Status | Possible codes |
|---|---|
| 400 | invalid_input |
| 401 | unauthenticated |
| 402 | plan_limit |
| 403 | forbidden / workspace_not_found |
| 404 | not_found |
| 409 | no_account / needs_reauth / replay |
| 429 | rate_limited |
| 500 | internal |
| 502 | platform_error |
| 503 | external_writes_off |
Examples
curl -X POST "https://popkit.loxily.com/api/v1/posts" \
-H "Authorization: Bearer popkit_it_••••••••" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: acme-2026-09-21-001" \
-d '{
"action": "schedule",
"content": "新功能上线:一次创作,发布到所有平台 🚀",
"platforms": [
"x",
"instagram"
],
"scheduledAt": "2026-09-21T18:00:00+08:00",
"media": [
{
"kind": "image",
"dataUrl": "https://storage.googleapis.com/…/cover.jpg"
}
],
"variants": [
{
"platform": "x",
"content": "新功能上线 🚀 一次创作,发布到所有平台 → popkit.loxily.com"
}
]
}'{
"data": {
"postId": "pst_01J8ZK3R7QW6M2",
"status": "scheduled",
"scheduledAt": "2026-09-21T10:00:00.000Z",
"deepLink": "https://popkit.loxily.com/calendar?post=pst_01J8ZK3R7QW6M2"
},
"meta": {
"requestId": "3f9c1e2a-7d4b-4c1e-9a6f-0b2d8e5c7a11"
}
}