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
| Property | Value |
|---|---|
| Character limit | 280 |
| Images per post | 4 (or 1 GIF) |
| Videos per post | Not yet supported via Synposter |
| Image formats | PNG, JPEG, WebP, GIF |
| Image max size | 5 MB |
| GIF max size | 15 MB |
| Threads | Yes (via threadItems) |
| Replies | Yes (via replyToTweetId) |
| Community posts | Yes (via communityId) |
| Polls | Yes (via poll) |
| Scheduling | Coming soon |
| Analytics | Coming 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"
}'{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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).
{
"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.
{
"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:
| Property | Value |
|---|---|
| tweet.read | Read tweets posted by or visible to the user. |
| tweet.write | Publish tweets on the user's behalf. |
| users.read | Read the user's id, username, and display name (used to populate the connected-account record). |
| offline.access | Issue a refresh token so Synposter can keep the account connected without re-prompting the user. |