Skip to content
DunSocial Docs

Schedule and publish

Create scheduled posts, publish immediately, and post X/Twitter threads through the DunSocial API, using bearer auth and your workspace id header.

Post routes need Bearer auth and X-Workspace-Id.

Schedule a post

POST /api/posts/schedule

curl -X POST https://api.dunsocial.com/api/posts/schedule \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Workspace-Id: YOUR_WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
  "socialAccountIds": ["ACCOUNT_ID"],
  "content": "Shipping something small today.",
  "scheduledAt": "2026-08-01T15:00:00.000Z",
  "mediaUrls": []
}'

Returns 201 with one post per social account. Validation is all-or-nothing: a character-limit or platform-metadata error creates zero posts. After validation passes, each account is inserted and queued. If QStash fails mid-batch, earlier accounts in that groupId may already be scheduled.

Validate without writing

POST /api/posts/validate

Same body as schedule / publish-now, without scheduledAt. Runs the same media, character-limit, per-platform metadata, and X monthly-cap checks. Does not create posts or queue jobs.

curl -X POST https://api.dunsocial.com/api/posts/validate \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Workspace-Id: YOUR_WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
  "socialAccountIds": ["ACCOUNT_ID"],
  "content": "Shipping something small today.",
  "mediaUrls": []
}'

Returns 200:

{
"success": true,
"data": {
  "valid": false,
  "issues": [
    {
      "socialAccountId": "ACCOUNT_ID",
      "platform": "X",
      "code": "content_length",
      "message": "Content exceeds X character limit: 283/280 characters"
    }
  ]
}
}

valid: true and an empty issues array means the same payload can be sent to schedule or publish-now.

Publish now

POST /api/posts/publish-now

Same body as schedule, without scheduledAt. Optional naturalPosting boolean.

Threads (X only)

Method Path
POST /api/posts/schedule-thread
POST /api/posts/publish-thread-now

Body shape:

{
"socialAccountId": "ACCOUNT_ID",
"scheduledAt": "2026-08-01T15:00:00.000Z",
"tweets": [
  { "content": "First tweet", "mediaUrls": [] },
  { "content": "Second tweet" }
]
}

Threads need 2–25 tweets. Response includes threadHeadPost, totalTweets, and postIds.

Required per-platform metadata

When socialAccountIds includes that platform, the matching metadata block is required. There is no default Instagram format (do not assume reel).

Platform Required field Required keys Notes
Reddit metadata.reddit subreddit, title, kind kind: self, link, image, or video. Flair when the subreddit requires it.
Instagram metadata.instagram format format: feed_image, carousel, reel, or story. Optional caption (max 2200, at most 5 hashtags). Feed/carousel/story images: JPEG, PNG, GIF, or WebP (non-JPEG converts at publish).
Pinterest metadata.pinterest boardId, title, description Exactly one image.
TikTok metadata.tiktok privacyLevel Exactly one video. Optional title (max 2200).
Discord metadata.discord channelId Text or announcement channel from the connected server. Optional channelName. Up to 10 attachments; total request max 25 MB.
Slack metadata.slack channelId Public or private channel from the connected Slack team. Optional channelName. Up to 10 attachments; 50 MB per file.

X, LinkedIn, Bluesky, Threads, YouTube, and Facebook Pages do not need a metadata block for a text/media post. Facebook Pages are text, one link, and photos only (no video).

Character limits (on content)

Platform Limit
X 280, or 25,000 when the connected account is X Premium
Bluesky 300
Threads 500
LinkedIn 3,000
Instagram 2,200 (caption in metadata.instagram is also capped at 2,200)
TikTok 2,200
Facebook Pages 63,206 (images only; no metadata.facebook)
Pinterest 800 (metadata.pinterest.description also 800; title 100)
YouTube 5,000 description (title 100, handled separately)
Reddit 40,000 (metadata.reddit.title max 300)
Discord 2,000
Slack 4,000

Media on a post

mediaUrls can be media asset ids, https://media.dunsocial.com/... URLs, or storage keys like media/uploads/.... Stored and returned posts keep asset ids in mediaUrls.

Common errors

Status Cause
400 Character limits, missing/invalid platform options, bad media
404 Media asset missing
429 X monthly posting cap

Check cap usage: GET /api/posts/x-cap-usage. Dry-run the same checks with POST /api/posts/validate.