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)

  1. In the X developer console, create a Project and an App inside it.
  2. Under User authentication settings, set App permissions to Read and write and Type of App to Web App, Automated App or Bot (confidential client).
  3. Add the callback URI https://social-publish-kappa.vercel.app/api/integrations/twitter/callback (or set TWITTER_REDIRECT_URI to override it).
  4. Copy the OAuth 2.0 Client ID and Client Secret into TWITTER_CLIENT_ID and TWITTER_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

RuleLimit
Text length280 weighted characters; 25,000 with longPost: true
WeightingEvery URL counts as 23; emoji and CJK characters count as 2
Text or mediaA post needs text or media
ImagesUp to 4 per post, 5 MB each
GIFs1 per post, no other media, up to 15 MB
Video1 per post, no other media, up to 512 MB
Alt textSent for images and GIFs (media[].altText, up to 1,000 characters)
ThreadUp to 24 extra posts; each follows the same length limit as the main post
Poll2–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

OptionTypeRequiredDefaultDescription
communityIdstringNoCommunity ID. Post to a Twitter Community (optional). Paste the Community ID or its x.com/i/communities/… URL; you must be a member.
shareWithFollowersbooleanNofalseShare with followers. Also share the Community post with your followers. Only applies when a Community ID is set.
replySettingseveryone \following \mentionedUsers \subscribers \
longPostbooleanNofalseLong post (Premium). Allow over 280 characters. Requires an X Premium / Premium+ account; X rejects it otherwise.
threadobject[]NoThread. 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).
pollOptionsobject[]NoAdd a poll. 2–4 different options, up to 25 characters each. Polls can't be combined with media. Items: label (max 4).
pollDurationMinutesnumberNo1440Poll duration (minutes). Only used when poll options are added. 5 minutes to 7 days (10080); default 1 day.
quoteTweetstringNoQuote post. Optional post URL or ID to quote. X may restrict quoting via the API on some access tiers.
madeWithAibooleanNofalseMade 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

Request body
{
  "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

Request body
{
  "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

ErrorCause
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 mediaA video was combined with other media.
X allows one GIF per post, without other mediaA GIF (URL ending in .gif) was combined with other media.
X posts can't have both a poll and mediaRemove the media or the poll.
Share with followers needs a Community IDshareWithFollowers 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 messageThe X developer app has no paid access or credits.
Account becomes reconnect_requiredThe 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.