Platforms
X (Twitter)
Post text, images, GIFs, video, threads, polls and quote posts to X through the X API v2.
Overview
The twitter platform publishes to an X account with the X API v2. You can post:
- Text posts (280 weighted characters, or 25,000 with Long post on Premium accounts)
- Up to 4 images, or 1 GIF, or 1 video
- Threads: extra posts chained as replies to the first one
- Polls with 2–4 options
- Quote posts, Community posts, reply restrictions and an AI-generated media label
Connect the account
In the dashboard, open Integrations, choose X (Twitter) and approve access on X. Each X login connects one account.
App setup (self-hosted)
- In the X developer console, create a Project and an App inside it.
- Under User authentication settings, set App permissions to Read and write and Type of App to Web App, Automated App or Bot (confidential client).
- Add the callback URI
https://social-publish-kappa.vercel.app/api/integrations/twitter/callback(or setTWITTER_REDIRECT_URIto override it). - Copy the OAuth 2.0 Client ID and Client Secret into
TWITTER_CLIENT_IDandTWITTER_CLIENT_SECRET.
Scopes requested: tweet.read, tweet.write, users.read, media.write, offline.access. Access tokens last about two hours and are refreshed automatically; X rotates the refresh token on every refresh.
Posting and media upload are billed by X. If the developer app has no credits or plan, X answers with 402/403 and the target fails with a message pointing at the X developer console.
Post types & media rules
| Rule | Limit |
|---|---|
| Text length | 280 weighted characters; 25,000 with longPost: true |
| Weighting | Every URL counts as 23; emoji and CJK characters count as 2 |
| Text or media | A post needs text or media |
| Images | Up to 4 per post, 5 MB each |
| GIFs | 1 per post, no other media, up to 15 MB |
| Video | 1 per post, no other media, up to 512 MB |
| Alt text | Sent for images and GIFs (media[].altText, up to 1,000 characters) |
| Thread | Up to 24 extra posts; each follows the same length limit as the main post |
| Poll | 2–4 different options, 25 characters each, 5 minutes to 7 days; cannot be combined with media |
Thread items are text only. Media and the poll attach to the first post.
Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
communityId | string | No | Community ID. Post to a Twitter Community (optional). Paste the Community ID or its x.com/i/communities/… URL; you must be a member. | |
shareWithFollowers | boolean | No | false | Share with followers. Also share the Community post with your followers. Only applies when a Community ID is set. |
replySettings | everyone \ | following \ | mentionedUsers \ | subscribers \ |
longPost | boolean | No | false | Long post (Premium). Allow over 280 characters. Requires an X Premium / Premium+ account; X rejects it otherwise. |
thread | object[] | No | Thread. Add more tweets as a thread. Each is posted as a reply to the previous one, in order. If a later tweet fails, the first one stays live. Items: text (max 24). | |
pollOptions | object[] | No | Add a poll. 2–4 different options, up to 25 characters each. Polls can't be combined with media. Items: label (max 4). | |
pollDurationMinutes | number | No | 1440 | Poll duration (minutes). Only used when poll options are added. 5 minutes to 7 days (10080); default 1 day. |
quoteTweet | string | No | Quote post. Optional post URL or ID to quote. X may restrict quoting via the API on some access tiers. | |
madeWithAi | boolean | No | false | Made with AI. Label the attached media as AI-generated. |
quoteTweet accepts a numeric post ID or an x.com / twitter.com status URL. communityId accepts a numeric ID or an x.com/i/communities/<id> URL, and shareWithFollowers requires a Community ID. |
Examples
Thread with an image
curl -X POST https://social-publish-api.onrender.com/v1/posts \
-H "x-access-key: $RELAYCAST_API_KEY" \
-H "content-type: application/json" \
-d '{
"content": "We just shipped v2.0. Here is what changed 🧵",
"media": [{ "url": "https://cdn.example.com/launch.png", "type": "image", "altText": "Release notes screenshot" }],
"targets": [{
"accountId": "6f1c2a7e-4b8d-4f7a-9a51-2d3c4b5a6e7f",
"options": {
"replySettings": "following",
"thread": [
{ "text": "1. Faster uploads for large videos." },
{ "text": "2. Scheduled posts now show in your calendar." }
]
}
}],
"publishNow": true
}'const res = await fetch("https://social-publish-api.onrender.com/v1/posts", {
method: "POST",
headers: {
"x-access-key": process.env.RELAYCAST_API_KEY!,
"content-type": "application/json",
},
body: JSON.stringify({
content: "We just shipped v2.0. Here is what changed 🧵",
media: [{ url: "https://cdn.example.com/launch.png", type: "image", altText: "Release notes screenshot" }],
targets: [
{
accountId: "6f1c2a7e-4b8d-4f7a-9a51-2d3c4b5a6e7f",
options: {
replySettings: "following",
thread: [
{ text: "1. Faster uploads for large videos." },
{ text: "2. Scheduled posts now show in your calendar." },
],
},
},
],
publishNow: true,
}),
});
const { data } = await res.json();import os, requests
res = requests.post(
"https://social-publish-api.onrender.com/v1/posts",
headers={"x-access-key": os.environ["RELAYCAST_API_KEY"]},
json={
"content": "We just shipped v2.0. Here is what changed 🧵",
"media": [{"url": "https://cdn.example.com/launch.png", "type": "image", "altText": "Release notes screenshot"}],
"targets": [{
"accountId": "6f1c2a7e-4b8d-4f7a-9a51-2d3c4b5a6e7f",
"options": {
"replySettings": "following",
"thread": [
{"text": "1. Faster uploads for large videos."},
{"text": "2. Scheduled posts now show in your calendar."},
],
},
}],
"publishNow": True,
},
)
data = res.json()["data"]Poll
{
"content": "Which feature should we build next?",
"targets": [{
"accountId": "6f1c2a7e-4b8d-4f7a-9a51-2d3c4b5a6e7f",
"options": {
"pollOptions": [{ "label": "Dark mode" }, { "label": "Calendar view" }, { "label": "CSV export" }],
"pollDurationMinutes": 2880
}
}],
"scheduledFor": "2026-11-02T15:00:00Z"
}Quote post in a Community
{
"content": "Great write-up on API design.",
"targets": [{
"accountId": "6f1c2a7e-4b8d-4f7a-9a51-2d3c4b5a6e7f",
"options": {
"quoteTweet": "https://x.com/example/status/1850000000000000000",
"communityId": "1493446837214187523",
"shareWithFollowers": true
}
}],
"publishNow": true
}Analytics & comments
Post analytics are available from Posts → Analytics in the dashboard: impressions, likes, replies (comments), reposts + quotes (shares) and bookmarks (saves). Link and profile clicks are only returned by X for your own posts within 30 days of posting. Each analytics load is a billed post read on X's pay-per-use pricing.
Comments and DMs are not supported for X.
Limitations & common errors
| Error | Cause |
|---|---|
X posts allow 280 characters … | Text is over the weighted limit. Shorten it or set longPost: true on a Premium account. |
X allows one video per post, without other media | A video was combined with other media. |
X allows one GIF per post, without other media | A GIF (URL ending in .gif) was combined with other media. |
X posts can't have both a poll and media | Remove the media or the poll. |
Share with followers needs a Community ID | shareWithFollowers without communityId. |
| Target fails with "duplicate of a recent post" | X rejects identical text posted shortly after an earlier post. |
| Target fails with a plan / credits message | The X developer app has no paid access or credits. |
Account becomes reconnect_required | The token was revoked or expired without a valid refresh token. Reconnect from Integrations. |
If a later post in a thread fails, the first post stays live and the target is still marked published with the first post's URL. The remaining thread posts are skipped rather than retried, so the first post is never duplicated.
This page is also available as Markdown.