How to Schedule X (Twitter) Posts with an API (2026 Guide)


If you want to schedule posts on X (formerly Twitter) via an API, you need to understand one thing first: the X API is no longer free for posting. Since the 2023 pricing overhaul, sending a single tweet through the API requires a $100/month Basic plan at minimum. There is no free posting tier. No trial. No workaround.
That changes how you should approach this. Depending on your use case, the native X API v2 might still be the right call — or a unified social media scheduling API that includes X posting at a fraction of the cost might save you thousands per year.
This guide covers both options with working code examples, authentication walkthroughs, and an honest pricing comparison.
Table of Contents
- X API Pricing Tiers (2026)
- What You Can and Cannot Do with the X API
- Option 1: Schedule X Posts with the Native X API v2
- Option 2: Schedule X Posts with PostEverywhere's API
- Pricing Comparison: X API vs PostEverywhere API
- X API Changes Timeline
- Thread Posting via the X API
- Media Uploads on X
- Rate Limits and Gotchas
- FAQs
X API Pricing Tiers (2026)
Before you write a line of code, understand what you are paying for. X restructured its API pricing in March 2023, and the tiers have remained largely unchanged since:
| Tier | Monthly Cost | Tweet Read Limit | Tweet Post Limit | User Auth | Posting Enabled |
|---|---|---|---|---|---|
| Free | $0 | 1,500 tweets/month | 0 | 1 app | No |
| Basic | $100/month | 50,000 tweets/month | 1,667 tweets/month | 1 user | Yes |
| Pro | $5,000/month | 1,000,000 tweets/month | 300,000 tweets/month | Full | Yes |
| Enterprise | Custom | Custom | Custom | Full | Yes |
The Free tier gives you read-only access to 1,500 tweets per month. That is it. You cannot create tweets, schedule tweets, or interact with tweets programmatically on Free. If you want to post anything, you need Basic at $100/month minimum.
For context: the Instagram Graph API, LinkedIn API, and Threads API all allow posting for free. The YouTube Data API allows uploads for free. X is the only major platform that charges $100/month just to send a programmatic post.
This is the single biggest factor in choosing how to build X scheduling into your product.
What You Can and Cannot Do with the X API
Before diving into code, here is what the X API v2 supports and what it does not:
What you CAN do:
- Create tweets (text, with media, with polls)
- Delete tweets
- Reply to tweets (thread creation)
- Upload images and video (via the v1.1 media upload endpoint)
- Like and retweet programmatically
- Read timelines, search tweets, stream tweets
What you CANNOT do:
- Schedule tweets natively — the X API has no scheduling endpoint. There is no
scheduled_atparameter. If you want to schedule a post for 3pm tomorrow, you need to build your own scheduling infrastructure: a queue, a cron job or task runner, persistent storage, and retry logic. - Edit tweets via API (editing is only available through the X client)
- Access DMs on Basic tier
- Post from multiple user accounts on Basic tier
The lack of native scheduling is a big deal. Every developer building X scheduling has to solve the same infrastructure problem independently — or use a tool that has already solved it.
Option 1: Schedule X Posts with the Native X API v2
If you want full control and are comfortable with the $100/month baseline, here is how to post to X using the native API.
Step 1: Create a Developer Account and App
- Go to the X Developer Portal
- Sign up for the Basic plan ($100/month)
- Create a new Project and App
- Save your API Key, API Secret, Bearer Token, Access Token, and Access Token Secret
Step 2: Set Up OAuth 2.0 with PKCE
X API v2 uses OAuth 2.0 with PKCE (Proof Key for Code Exchange) for user-context authentication. This is required for posting tweets on behalf of a user.
// OAuth 2.0 PKCE flow for X API v2
import crypto from 'crypto';
const CLIENT_ID = process.env.X_CLIENT_ID;
const REDIRECT_URI = 'https://yourapp.com/callback';
const SCOPES = 'tweet.read tweet.write users.read offline.access';
// Step 1: Generate PKCE challenge
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
return { verifier, challenge };
}
// Step 2: Build authorization URL
function getAuthUrl() {
const { verifier, challenge } = generatePKCE();
const state = crypto.randomBytes(16).toString('hex');
// Store verifier and state in session
const authUrl = new URL('https://twitter.com/i/oauth2/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('scope', SCOPES);
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
return { url: authUrl.toString(), verifier, state };
}
// Step 3: Exchange code for access token
async function exchangeCodeForToken(code, verifier) {
const response = await fetch('https://api.x.com/2/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
code,
grant_type: 'authorization_code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
code_verifier: verifier,
}),
});
return response.json();
// Returns: { access_token, refresh_token, expires_in, token_type, scope }
}
You will need offline.access scope to get a refresh token. X access tokens expire after 2 hours, so your scheduling system needs to handle token refreshes automatically.
Step 3: Post a Tweet
Once you have an access token, posting is a single API call:
async function postTweet(accessToken, text) {
const response = await fetch('https://api.x.com/2/tweets', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`X API error: ${JSON.stringify(error)}`);
}
return response.json();
// Returns: { data: { id: "1234567890", text: "Your tweet" } }
}
Step 4: Build Your Own Scheduling Layer
Since the X API has no native scheduling, you need to build it yourself. Here is a minimal example using a job queue:
import { Queue, Worker } from 'bullmq';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
const scheduledPostsQueue = new Queue('x-scheduled-posts', {
connection: redis,
});
// Schedule a post for a future time
async function scheduleXPost(userId, text, publishAt, mediaIds = []) {
const delay = new Date(publishAt).getTime() - Date.now();
if (delay <= 0) {
throw new Error('Publish time must be in the future');
}
await scheduledPostsQueue.add(
'publish-tweet',
{ userId, text, mediaIds },
{ delay, attempts: 3, backoff: { type: 'exponential', delay: 5000 } }
);
}
// Worker that processes scheduled posts
const worker = new Worker('x-scheduled-posts', async (job) => {
const { userId, text, mediaIds } = job.data;
// Retrieve and refresh access token from your database
const accessToken = await getValidAccessToken(userId);
const body = { text };
if (mediaIds.length > 0) {
body.media = { media_ids: mediaIds };
}
const response = await fetch('https://api.x.com/2/tweets', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Failed to post: ${response.status}`);
}
return response.json();
}, { connection: redis });
That is a minimal setup. In production, you also need: token refresh logic, dead letter queues, monitoring, timezone handling, duplicate prevention, and rate limit awareness. It adds up fast.
Option 2: Schedule X Posts with PostEverywhere's API
If you want scheduling without building the infrastructure yourself — or if paying $100/month for a single platform does not make sense for your project — the PostEverywhere API handles X posting and scheduling out of the box.
What You Get
- Scheduling is native: pass a
scheduledTimeparameter and it just works. No queues, no cron jobs, no Redis. - One API for 8 platforms: X, Instagram, LinkedIn, Facebook, TikTok, YouTube, Threads, and Pinterest. One authentication flow, one posting endpoint.
- $19/month covers posting to all platforms — vs $100/month for X API alone.
- Token management, retry logic, rate limiting, and media processing are handled for you.
Code Example: Schedule an X Post
async function scheduleXPost(text, scheduledTime, imageUrl) {
const response = await fetch('https://api.posteverywhere.com/v1/posts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
platforms: ['x'],
content: {
text,
mediaUrls: imageUrl ? [imageUrl] : [],
},
scheduledTime, // ISO 8601: "2026-04-14T15:00:00Z"
}),
});
return response.json();
// Returns: { id: "post_abc123", status: "scheduled", platforms: ["x"], scheduledTime: "..." }
}
// Schedule a post for tomorrow at 3pm UTC
await scheduleXPost(
'Shipping a new feature today. Details in the thread.',
'2026-04-14T15:00:00Z'
);
That is it. No OAuth dance, no token refresh, no job queue, no Redis. One API call.
Cross-Platform Scheduling
The real advantage shows when you are posting to multiple platforms. With native APIs, you would need to manage authentication and posting logic for each platform separately. With PostEverywhere, you pass an array:
const response = await fetch('https://api.posteverywhere.com/v1/posts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
platforms: ['x', 'linkedin', 'threads', 'instagram'],
content: {
text: 'We just launched v2.0. Here is what changed.',
mediaUrls: ['https://yourcdn.com/launch-image.jpg'],
},
scheduledTime: '2026-04-14T15:00:00Z',
}),
});
One request. Four platforms. All scheduled. Check out the full PostEverywhere API documentation for endpoint details, webhooks, and more.
Building an app that posts to X? PostEverywhere's API gives you scheduling, cross-posting, and media handling for $19/month — instead of $100/month for X API Basic alone. Start with a 7-day free trial.
Pricing Comparison: X API vs PostEverywhere API
This is the table that matters. If you are evaluating how to add X scheduling to your product, cost is probably the deciding factor:
| Feature | X API (Basic) | PostEverywhere API (Starter) |
|---|---|---|
| Monthly cost | $100/month | $19/month |
| Platforms included | X only | X, Instagram, LinkedIn, Facebook, TikTok, YouTube, Threads, Pinterest |
| Native scheduling | No (build your own) | Yes (pass scheduledTime) |
| Tweet post limit | 1,667/month | Based on plan |
| OAuth complexity | OAuth 2.0 PKCE per user | API key |
| Token refresh | You manage (2-hour expiry) | Handled automatically |
| Media upload | Separate v1.1 endpoint | Pass a URL |
| Thread support | Manual sequential calls | Thread parameter |
| Rate limit handling | You implement | Built-in |
| Infrastructure required | Redis/queue + cron + DB | None |
| Annual cost (X only) | $1,200/year | $228/year |
If you are building a product that only needs X and you need the full 50,000 tweets/month read access for analytics or data purposes, the native X API makes sense. For everything else — especially if you are posting to multiple platforms — the economics strongly favor a unified API.
For teams that need more capacity, PostEverywhere's Growth plan at $39/month gives you 25 connected accounts and 500 AI credits. The Pro plan at $79/month covers 40 accounts. All three plans are still cheaper than X API Basic alone. See pricing details.
X API Changes Timeline
Understanding how we got here helps you plan for where things are going:
| Date | Change | Impact |
|---|---|---|
| 2006-2020 | Twitter API free for all developers | The golden era. Anyone could build Twitter clients, bots, scheduling tools. No cost. |
| 2020 | Twitter API v2 launched | New endpoints, OAuth 2.0, better data model. v1.1 remained available. |
| Oct 2022 | Elon Musk acquires Twitter | Signalled major changes ahead. |
| Feb 2023 | Free API access revoked | The free tier was stripped to read-only with severe limits. Thousands of bots and tools broke overnight. |
| Mar 2023 | New pricing tiers launched | Basic ($100/month), Pro ($5,000/month), Enterprise (custom). Posting requires paid tier. |
| 2023-2024 | Rebranded to X | API endpoints migrated from api.twitter.com to api.x.com (old URLs still work). |
| 2024-2026 | Gradual stabilisation | Pricing unchanged. Some v1.1 endpoints deprecated. OAuth 2.0 with PKCE became the standard auth flow. |
The lesson: X's API strategy is unpredictable. Pricing could change again. If you are building a product that depends on X posting, having an abstraction layer (like a unified scheduling API) insulates you from future breaking changes.
Thread Posting via the X API
Threads (multi-tweet sequences) are a common scheduling need. The X API does not have a native "create thread" endpoint. You post each tweet individually and chain them using reply parameters:
async function postThread(accessToken, tweets) {
const postedTweets = [];
for (let i = 0; i < tweets.length; i++) {
const body = { text: tweets[i] };
// After the first tweet, reply to the previous one
if (i > 0) {
body.reply = {
in_reply_to_tweet_id: postedTweets[i - 1].data.id,
};
}
const response = await fetch('https://api.x.com/2/tweets', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(
`Failed to post tweet ${i + 1}: ${response.status}`
);
}
const result = await response.json();
postedTweets.push(result);
// Small delay to avoid rate limits
if (i < tweets.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
return postedTweets;
}
// Usage
await postThread(accessToken, [
'1/ We just shipped the biggest update this year. Here is what changed:',
'2/ First, scheduling now supports threads natively. Queue up a 10-part thread and it posts sequentially at your chosen time.',
'3/ Second, AI-generated alt text for every image. Accessibility matters and we automated it.',
'4/ Third, analytics now show engagement by time-of-day. Find your best posting window automatically.',
'5/ Try it free for 7 days. No credit card required. Link below.',
]);
The gotcha with threads: if any tweet in the sequence fails (rate limit, content policy, network error), your thread is broken. You need retry logic per-tweet and a way to resume from the point of failure. With the PostEverywhere API, you pass an array of thread parts and the API handles sequencing, retries, and failure recovery.
Media Uploads on X
Media upload is another area where X's API shows its age. The tweet creation endpoint (POST /2/tweets) accepts media_ids — but you have to upload the media first using the v1.1 media upload endpoint. Yes, even though you are using API v2 for everything else, media upload is still on v1.1 and uses OAuth 1.0a authentication.
import OAuth from 'oauth-1.0a';
import crypto from 'crypto';
import FormData from 'form-data';
import fs from 'fs';
// Media upload still uses OAuth 1.0a and the v1.1 endpoint
const oauth = new OAuth({
consumer: {
key: process.env.X_API_KEY,
secret: process.env.X_API_SECRET,
},
signature_method: 'HMAC-SHA256',
hash_function(baseString, key) {
return crypto.createHmac('sha256', key).update(baseString).digest('base64');
},
});
async function uploadMedia(filePath) {
const mediaData = fs.readFileSync(filePath);
const form = new FormData();
form.append('media_data', mediaData.toString('base64'));
const url = 'https://upload.twitter.com/1.1/media/upload.json';
const authHeader = oauth.toHeader(
oauth.authorize({ url, method: 'POST' }, {
key: process.env.X_ACCESS_TOKEN,
secret: process.env.X_ACCESS_TOKEN_SECRET,
})
);
const response = await fetch(url, {
method: 'POST',
headers: { ...authHeader },
body: form,
});
const result = await response.json();
return result.media_id_string;
}
// Post a tweet with an image
async function postTweetWithImage(accessToken, text, imagePath) {
const mediaId = await uploadMedia(imagePath);
return postTweet(accessToken, text, {
media: { media_ids: [mediaId] },
});
}
This means your application needs two different authentication methods: OAuth 2.0 with PKCE for posting tweets and OAuth 1.0a for uploading media. It is a genuine pain point that has persisted for years.
For video uploads over 15MB, you also need to use the chunked upload process — a multi-step flow involving INIT, APPEND, FINALIZE, and STATUS commands. That is another layer of complexity.
With the PostEverywhere API, media handling is a URL parameter. You pass the image or video URL and the API handles uploading, processing, and format validation across all platforms.
Tired of managing two auth methods just to post a tweet with an image? The PostEverywhere API handles media uploads, scheduling, and cross-platform posting in one request. See the docs.
Rate Limits and Gotchas
The X API has several limits you need to handle in your scheduling system:
Posting limits (Basic tier):
- 1,667 tweets per month (about 55/day)
- Rate limit: 200 requests per 15-minute window for tweet creation
- User rate limit: 100 tweets per 24 hours per user
Common gotchas:
Duplicate content detection. X rejects tweets that are identical to recently posted content. If you are scheduling recurring content or running A/B tests, you need to vary the text slightly.
Token expiry. OAuth 2.0 access tokens expire after 2 hours. Your scheduling system must refresh tokens before posting. If you are scheduling a post for next week, you cannot store the current access token — you need to store the refresh token and exchange it at publish time.
URL length. X wraps all URLs in t.co links that count as 23 characters, regardless of the actual URL length. Factor this into your character count logic.
Media processing time. After uploading media (especially video), there is a processing delay before the
media_idis ready to use in a tweet. You need to poll the status endpoint until processing completes.App-level vs user-level limits. On the Basic tier, you get one user authentication. That means one X account. If you need to post on behalf of multiple users, you need Pro ($5,000/month) or Enterprise.
v1.1 deprecation. X has been gradually deprecating v1.1 endpoints, but media upload still requires it. Monitor the X API changelog for breaking changes.
These are not dealbreakers, but they add engineering time. For a social media scheduling tool that handles all of this automatically, PostEverywhere abstracts away rate limits, token management, and media processing across all platforms.
FAQs
Can I schedule tweets for free using the X API?
No. The free tier of the X API does not allow posting. You need the Basic plan at $100/month minimum to create tweets programmatically. There are no workarounds — if you want to schedule tweets via any API (native or third-party), the underlying service needs a paid X API tier or an existing connection to X.
Does the X API have a native scheduling endpoint?
No. The X API v2 has no scheduling functionality. You can create tweets immediately with POST /2/tweets, but there is no scheduled_at parameter or scheduling queue. To schedule posts, you need to build your own scheduling infrastructure (job queue, cron, database) or use a third-party API like the PostEverywhere API that provides native scheduling.
Why is the X API so much more expensive than other platforms?
After Elon Musk acquired Twitter in 2022, the company moved to a paid API model to generate revenue and reduce bot activity. Other platforms (Instagram, LinkedIn, YouTube, Threads) still offer free posting through their APIs because they are subsidised by their parent companies' ad businesses. X is the outlier.
Can I post to multiple X accounts on the Basic plan?
No. The Basic plan ($100/month) supports only one user authentication. If you need to post on behalf of multiple X accounts, you need the Pro plan ($5,000/month) or Enterprise. This is another reason developers opt for unified APIs — PostEverywhere's Starter plan supports up to 10 connected accounts across all platforms for $19/month.
What happens if the X API changes pricing again?
It is a real risk. X has changed its API terms multiple times since 2023. If you build directly on the X API, a pricing change could break your economics overnight. Using an abstraction layer like a unified scheduling API gives you some insulation — the API provider absorbs the change and you get a stable interface.
How do I post a thread (tweetstorm) via the API?
You post each tweet individually and chain them by setting the reply.in_reply_to_tweet_id field to the ID of the previous tweet. There is no batch endpoint. See the thread posting code example above. The main risk is partial failure — if tweet 3 of 5 fails, you have a broken thread.
Is OAuth 1.0a or OAuth 2.0 required for the X API?
Both, depending on the endpoint. Tweet creation uses OAuth 2.0 with PKCE. Media upload still uses the v1.1 endpoint with OAuth 1.0a. This means your application needs to implement two separate authentication flows, which adds complexity. The PostEverywhere API uses a single API key for all operations.
Can I use the X API to schedule posts with images and video?
Yes, but it requires a multi-step process. First, upload media to the v1.1 media upload endpoint (using OAuth 1.0a). For videos over 15MB, use the chunked upload flow. Then include the returned media_id in your tweet creation request (using OAuth 2.0). Your scheduling system needs to handle both upload and post creation at the scheduled time.
Wrapping Up
Scheduling X posts via an API in 2026 comes down to two decisions: how much control you need, and how much you want to spend.
If you need deep X-specific functionality — full read access for analytics, streaming, advanced search — the native X API v2 on the Basic or Pro tier is the way to go. Budget $100/month minimum and plan for significant engineering time building scheduling infrastructure, managing two auth methods, and handling rate limits.
If you are building a product that posts to X alongside other platforms, or you want scheduling without the infrastructure overhead, the PostEverywhere API covers X and 7 other platforms for $19/month with native scheduling, media handling, and a single authentication method.
Either way, X's API pricing makes it the most expensive platform to integrate with natively. Plan accordingly.
Check out the full social media scheduling API guide for a broader look at all platforms, or explore the best tools for scheduling on X if you want a dashboard-based approach instead. For non-developer scheduling options, see our guide on how to schedule posts on X.
Ready to start building? Explore the PostEverywhere API documentation and start your 7-day free trial.

Founder & CEO of PostEverywhere. Writing about social media strategy, publishing workflows, and analytics that help brands grow faster.