Platforms

Twitter / X

Post tweets, threads, polls, replies, and community tweets to Twitter / X through a single API endpoint. Synposter handles OAuth, token refresh, media upload, and the platform's rate limits.

Overview

Every X feature exposed by Synposter goes through a single endpoint, POST /v1/posts. Your end users authorize their account once via Synposter's OAuth flow; from then on you publish on their behalf without ever touching tokens or X developer apps yourself.

Quick reference

PropertyValue
Character limit280
Images per post4 (or 1 GIF)
Videos per postNot yet supported via Synposter
Image formatsPNG, JPEG, WebP, GIF
Image max size5 MB
GIF max size15 MB
ThreadsYes (via threadItems)
RepliesYes (via replyToTweetId)
Community postsYes (via communityId)
PollsYes (via poll)
SchedulingComing soon
AnalyticsComing soon

Before you start

X enforces a 280-character limit per tweet. URLs always count as 23 characters regardless of actual length. Emojis count as 2. Synposter trims whitespace before measuring, but doesn't shorten links — that's on X's side and it'll still happen even if your text fits 280 raw characters.

Duplicate tweets are rejected. X refuses tweets whose content matches a recent one closely (even minor variations). The retry comes back as platform_rejected.

The free X developer tier has its own quota. Roughly 17 tweets per 24-hour rolling window across your entire Synposter app, not per connected account. Hit that and every account in your batch comes back as platform_quota_exceeded. Upgrade your X dev plan or wait for the window to roll over — reconnecting an account doesn't help.

Quick start

Connect a Twitter account

Kick off the OAuth flow. Synposter sends back a URL to redirect the user to.

curl -X POST https://api.synposter.com/v1/connect/twitter \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "redirectUri": "https://yourapp.com/connected"
  }'
json
{
  "authUrl": "https://x.com/i/oauth2/authorize?response_type=code&client_id=..."
}

Redirect the user to authUrl. After approval, Synposter creates a connected-account record under the user's profile and redirects back to your redirectUri with accountId, profileId, and handle in the query string. See Connect a social account for the full flow.

Look up the account ID

List the accounts on a profile to find the one you just connected. The id on the record is what you pass to POST /v1/posts.

curl https://api.synposter.com/v1/accounts?profileId={profileId} \
  -H "x-api-key: YOUR_API_KEY"

Post a tweet

curl -X POST https://api.synposter.com/v1/posts \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accounts": [
      {
        "id": "acc_abc123",
        "platform": "twitter"
      }
    ],
    "text": "Hello from Synposter 🚀"
  }'

Content types

Every X feature is opt-in via the per-account platformOptionsobject. A request that doesn't set any of these fields publishes a plain text-or-media tweet.

Text tweet

The simplest case. Send text at the top level; X applies its own URL-shortening and emoji counting on top of the 280 limit.

json
{
  "accounts": [{ "id": "acc_abc123", "platform": "twitter"}],
  "text": "Just shipped a new feature."
}

Tweet with image

Pass a public media URL in the top-level media array. Synposter fetches the file and uploads it to X's media endpoint before publishing. Up to 4 images per tweet.

json
{
  "accounts": [{ "id": "acc_abc123", "platform": "twitter"}],
  "text": "Behind the scenes",
  "media": [
    { "type": "image", "url": "https://yourcdn.com/screenshot.png"}
  ]
}

If you don't host the file yourself, generate a presigned URL via POST /v1/media and reference its publicUrl.

Tweet with GIF

Same shape as an image, with type: "gif". X allows exactly 1 GIF per tweet, no mixing with images. The file size limit on X is 15 MB for animated GIFs.

json
{
  "accounts": [{ "id": "acc_abc123", "platform": "twitter"}],
  "text": "ship it",
  "media": [
    { "type": "gif", "url": "https://yourcdn.com/celebrate.gif"}
  ]
}

Thread (multi-tweet)

Publish a sequence of tweets that reply to each other in a chain. The top-level text is the first tweet; platformOptions.threadItems are tweets 2..N. Each item can carry its own text and optional media. Total chain length is capped at 25.

json
{
  "accounts": [{
    "id": "acc_abc123",
    "platform": "twitter",
    "platformOptions": {
      "threadItems": [
        { "text": "2/ Here's why it matters."},
        { "text": "3/ Try it free at synposter.com"}
      ]
    }
  }],
  "text": "1/ Just shipped 🚀"
}

Tweets are published sequentially. If item K fails (rate-limited, content rejected, etc.), items 1..K-1 stay live on X — there's no rollback. The per-account response lists every attempted tweet in order so you can decide whether to retry the failed tail.

Replies

Reply to a tweet

Set platformOptions.replyToTweetId to the X tweet id you want to reply to.

json
{
  "accounts": [{
    "id": "acc_abc123",
    "platform": "twitter",
    "platformOptions": {
      "replyToTweetId": "1748391029384756102"
    }
  }],
  "text": "Great point — agree completely."
}

Synposter doesn't pre-validate that the target tweet exists. If it's deleted or otherwise unreachable, X's rejection comes back as platform_rejected.

Reply with a thread

Combine replyToTweetId and threadItems to publish a thread that's a reply to an existing tweet. The first tweet replies to the target; subsequent items chain off the first.

json
{
  "accounts": [{
    "id": "acc_abc123",
    "platform": "twitter",
    "platformOptions": {
      "replyToTweetId": "1748391029384756102",
      "threadItems": [
        { "text": "2/ The full picture is..."},
        { "text": "3/ ...nuanced."}
      ]
    }
  }],
  "text": "1/ Reply"
}

Polls

Attach a poll via platformOptions.poll. 2-4 options, each up to 25 characters; durationMinutes between 5 (5 minutes) and 10080 (7 days).

json
{
  "accounts": [{
    "id": "acc_abc123",
    "platform": "twitter",
    "platformOptions": {
      "poll": {
        "options": ["React", "Vue", "Svelte", "Solid"],
        "durationMinutes": 1440
      }
    }
  }],
  "text": "Best framework for new projects?"
}

Polls are mutually exclusive with both media (rejected as poll_with_media) and threadItems (rejected as poll_in_thread) — X attaches a poll to a single tweet only. Polls + replies and polls + community posts both work fine.

Community posts

Publish into an X community by setting platformOptions.communityId. The connected account must be a member of the community; non-membership comes back as platform_rejectedwith X's message attached.

json
{
  "accounts": [{
    "id": "acc_abc123",
    "platform": "twitter",
    "platformOptions": {
      "communityId": "1234567890"
    }
  }],
  "text": "Sharing a draft for feedback."
}

For threads in a community, set communityId alongside threadItems. Synposter only stamps the id on the root tweet; X automatically inherits the community context for the chained replies.

OAuth scopes

When a user authorizes through POST /v1/connect/twitter, Synposter requests these scopes on X:

PropertyValue
tweet.readRead tweets posted by or visible to the user.
tweet.writePublish tweets on the user's behalf.
users.readRead the user's id, username, and display name (used to populate the connected-account record).
offline.accessIssue a refresh token so Synposter can keep the account connected without re-prompting the user.