Twitter API in Python: Post to X the Easy Way (2026)
Post tweets, threads and images to X from Python without an X developer app. Working SDK code, scheduling, retries, and when tweepy is the better pick.

You want a Python script that posts to X. The official route is an X developer app, four OAuth keys and tokens, and a prepaid credit balance with X before your first tweet goes out.
On this page
- How do you post to Twitter (X) with Python?
- What You Need
- Step 1: Install the SDK and Set Your API Key
- Step 2: Post Your First Tweet
- Character limits
- Step 3: Post a Thread (or One Long Post)
- Step 4: Add Images or a Video
- Step 5: Schedule Tweets in Your Own Time Zone
- Step 6: Read the Result
- Rate Limits, Retries and Errors
- Posts With Links Use Credits on X
- OmniSocials SDK vs. Tweepy and X's Own API
- Complete Script: Schedule a Week of Tweets
- Going Further
- Frequently Asked Questions
- How do I use the Twitter API with Python?
- Is the Twitter API free?
- Can I post to Twitter with Python without a developer account?
- What is the best Python library for the Twitter API?
- How do I schedule a tweet with Python?
- Can I post a Twitter thread with Python?
- Sources
There's a shorter route. This guide uses the official OmniSocials Python SDK to post tweets, threads, images and video to X, schedule them, and read the results. At the end, I'll compare it honestly with tweepy and X's own API, because for some jobs that is still the better tool.
How do you post to Twitter (X) with Python?
The quickest way: install the OmniSocials SDK with pip install omnisocials, connect your X account once in the OmniSocials dashboard, create an API key, and call client.posts.create(content="...", channels=["x"]). Add scheduled_at to schedule it, or leave it out to save a draft. No X developer app, no OAuth tokens to refresh. The OmniSocials Python SDK page has the full reference.
What You Need
- Python 3.9 or newer. The SDK's only dependency is
httpx. - An X account connected to OmniSocials. You sign in with X once in the dashboard. That's the whole X side of the setup.
- An OmniSocials API key. It belongs to one workspace and posts to the accounts connected there.
Step 1: Install the SDK and Set Your API Key
pip install omnisocials
The package is MIT licensed. It ships a sync client, OmniSocials, and an async one, AsyncOmniSocials, with the same method names.
Get your API key:
- Sign up at omnisocials.com and connect X under the workspace's channels (about a minute, standard X sign-in)
- Go to Settings > API
- Click Create API Key and copy it. It starts with
omsk_live_and is shown once. - Store it as an environment variable:
export OMNISOCIALS_API_KEY="omsk_live_your_key_here"
The client reads OMNISOCIALS_API_KEY on its own.
Note: There are no test keys. Every key posts to your real X account. While you experiment, leave out
scheduled_atso your posts stay drafts.
Step 2: Post Your First Tweet
from omnisocials import OmniSocials
client = OmniSocials() # reads OMNISOCIALS_API_KEY
post = client.posts.create(
content="Hello from Python. Scheduled with the OmniSocials SDK.",
channels=["x"],
scheduled_at="2027-03-02T09:00:00Z",
)
print(post["data"]["id"], post["data"]["status"]) # 1019 scheduled
A few things to know:
- The channel name is
x, nottwitter. - Responses are plain dicts with the post under
data. scheduled_atmust be in the future. A time in the past returns a400. Leave it out and the post is saved as a draft.
To publish right away, use create_and_publish with the same arguments, minus scheduled_at:
post = client.posts.create_and_publish(
content="Shipping day. The new release is out.",
channels=["x"],
)
print(post["data"]["status"]) # posting
Character limits
X allows 280 characters per post, or 25,000 when the connected account has X Premium or Premium+. OmniSocials reads your X plan when you connect, so reconnect X after you upgrade.
X counts characters its own way. Every link counts as 23 characters, whatever its real length, and emoji and some symbols count as 2. When you schedule or publish, the API checks your text against the right limit and answers 400 validation_error if it's too long.
Step 3: Post a Thread (or One Long Post)
Pass thread_parts in the x options to publish a chained thread. You can send 2 to 25 parts, and each one can carry its own media:
post = client.posts.create(
content="What we learned shipping 40 releases this year", # fallback text
channels=["x"],
scheduled_at="2027-03-02T15:00:00Z",
x={
"thread_parts": [
{"text": "What we learned shipping 40 releases this year. A thread:"},
{"text": "1/ Small releases beat big ones. Every single time."},
{
"text": "2/ Our release checklist fits on one screen.",
"media_urls": [
{
"url": "https://your-cdn.com/checklist.png",
"alt": "A one-page release checklist with 8 items",
}
],
},
{"text": "3/ The full write-up is on our blog. Link in our bio."},
]
},
)
OmniSocials posts the first part, then each next part as a reply to the one before it. Each part follows the same character limit as a single post, and takes up to 4 media items.
On a Premium account, you can skip the thread and send one long post instead. Just pass the full text as content, up to 25,000 characters.
Step 4: Add Images or a Video
X's media rules are strict, so it's worth knowing them up front:
- Up to 4 images, or one video. Not both in the same post.
- Images: JPEG, PNG or WebP, up to 5 MB each.
- Video: up to 140 seconds and 512 MB on a standard account. Premium accounts can post longer and larger videos.
The quickest way to attach an image is media_urls. OmniSocials downloads the file and attaches it to the post. Add an alt text and X shows it to screen-reader users:
post = client.posts.create(
content="Our Q1 signups in one chart.",
channels=["x"],
media_urls=[
{
"url": "https://your-cdn.com/q1-chart.png",
"alt": "Bar chart of Q1 signups by month, rising each month",
}
],
scheduled_at="2027-03-02T09:00:00Z",
)
media_urls takes files up to 100 MB. For a bigger video, upload it to your media library first (up to 1 GB from a URL) and pass its id:
video = client.media.upload_from_url(
url="https://your-cdn.com/editor-demo.mp4",
)
client.posts.create(
content="Two-minute tour of the new editor.",
channels=["x"],
media_ids=[video["data"]["id"]],
scheduled_at="2027-03-02T09:00:00Z",
)
For a file on disk, use client.media.upload(file="./q1-chart.png") instead. Direct uploads go up to 100 MB.
Step 5: Schedule Tweets in Your Own Time Zone
scheduled_at takes any ISO 8601 timestamp, with Z for UTC or an offset. Python's zoneinfo gives you the offset for free:
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
new_york = ZoneInfo("America/New_York")
tomorrow_9am = (datetime.now(new_york) + timedelta(days=1)).replace(
hour=9, minute=0, second=0, microsecond=0
)
post = client.posts.create(
content="Good morning, New York. Here's what's new this week.",
channels=["x"],
scheduled_at=tomorrow_9am.isoformat(), # e.g. 2027-03-02T09:00:00-05:00
)
Not sure when to post? Ask for your best slots on X:
best = client.analytics.best_times(platform="x")
print(best["recommendations"]) # top 3: day, time, score
This is the one call without the data wrapper: recommendations sits at the top level. Until your workspace has 15 analyzed X posts, the answer is based on industry averages instead of your own data.
Cross-posting? Add more channels and give X its own shorter text:
client.posts.create(
content={
"default": "We rebuilt our editor from scratch. Here's what changed and why it took a year.",
"x": "We rebuilt our editor from scratch. A year of work, one short thread soon.",
},
channels=["x", "linkedin", "bluesky"],
scheduled_at="2027-03-02T09:00:00Z",
)
Step 6: Read the Result
A post moves from scheduled to posting to published. If X rejects it, it ends as failed (or warning when other platforms in the same post did publish), with the reason in errors.
post = client.posts.get("1019")["data"]
print(post["status"]) # published
print(post["published_urls"].get("x")) # https://x.com/yourhandle/status/...
Metrics come from the analytics resource. Each platform reports its own metric names under platforms:
stats = client.analytics.post("1019")
x_stats = stats["data"]["platforms"].get("x")
if x_stats:
print(x_stats["metrics"])
Polling works for a script. For anything long-running, let a webhook tell you when a post is published or fails.
Rate Limits, Retries and Errors
The OmniSocials API allows 100 requests per minute per API key. The SDK already retries the failures worth retrying: 429, 5xx responses and network errors, twice by default, and it waits for the Retry-After header when the API sends one.
What's left for you is deciding what to do when a request is really rejected:
from omnisocials import (
AuthenticationError,
OmniSocials,
RateLimitError,
ValidationError,
)
client = OmniSocials(max_retries=5, timeout=60.0)
try:
client.posts.create(
content="Weekly changelog is live.",
channels=["x"],
scheduled_at="2027-03-02T09:00:00Z",
)
except AuthenticationError:
print("API key invalid or revoked. Check OMNISOCIALS_API_KEY.")
except ValidationError as err:
print(err.code, err.message) # e.g. platform_not_connected
except RateLimitError as err:
print(f"Still rate limited, retry in {err.retry_after}s")
X also limits how often an account can post. If X refuses a post at publish time, the post shows failed with X's reason in errors. Once you've fixed the cause, client.posts.retry(post_id) publishes it again, only to the platforms that failed.
For a deeper look at limits across platforms, see our social media API rate limits guide.
Posts With Links Use Credits on X
This one matters, so here it is plainly. X charges API posts that contain a link far more than plain posts: $0.20 versus $0.015 at the time of writing, according to X's API pricing.
OmniSocials covers the plain-post fee as part of the plan. For link posts, it passes X's fee on at cost, as prepaid credits:
- A post with a link that starts with
http://orhttps://uses 20 credits. - In a thread, each part with such a link uses 20 credits.
- A bare domain like
brand.comis free, and so are posts without links. - Credits are only deducted once the post publishes. You top them up in the dashboard under Settings > Billing > Credits.
When you create an X post with a link, the response carries a warnings list next to data. If scheduling the post would reserve more credits than you have, the API refuses it with a 402 and the code x_credits_insufficient:
from omnisocials import APIError
try:
res = client.posts.create(
content="New on the blog: https://example.com/release-notes",
channels=["x"],
scheduled_at="2027-03-02T09:00:00Z",
)
for warning in res.get("warnings", []):
if warning.get("code") == "x_url_post_credits":
print("This post uses", warning["credits_required"], "credits")
except APIError as err:
if err.code != "x_credits_insufficient":
raise
print(err.body["error"]["details"]) # required, balance, reserved
If you'd rather not use credits at all, the classic workaround still works: post without the link, and put the link in a reply or your bio.
OmniSocials SDK vs. Tweepy and X's Own API
Tweepy is the best-known Python library for X's own API. Posting a tweet with it looks like this:
import os
import tweepy
client = tweepy.Client(
consumer_key=os.environ["X_API_KEY"],
consumer_secret=os.environ["X_API_KEY_SECRET"],
access_token=os.environ["X_ACCESS_TOKEN"],
access_token_secret=os.environ["X_ACCESS_TOKEN_SECRET"],
)
response = client.create_tweet(text="Hello from tweepy")
print(response.data["id"])
The code is short. The setup around it isn't. You need an X developer account, an app with read and write permission, four keys and tokens, and credits in X's Developer Console. X bills every post you create from those credits.
| OmniSocials Python SDK | Tweepy + X API | |
|---|---|---|
| Setup | OmniSocials Python SDK: Sign in to X once, one API key | Tweepy + X API: Developer app, 4 OAuth keys and tokens |
| Plain post | OmniSocials Python SDK: Included in the plan | Tweepy + X API: Billed per post by X |
| Post with a link | OmniSocials Python SDK: 20 credits, X's rate at cost | Tweepy + X API: Billed per post by X, at the higher link rate |
| Scheduling | OmniSocials Python SDK: scheduled_at | Tweepy + X API: Your own cron job or queue |
| Threads | OmniSocials Python SDK: One call with thread_parts | Tweepy + X API: One call per tweet, chained by reply id |
| Media | OmniSocials Python SDK: In the same call | Tweepy + X API: Separate upload call first |
| Search, timelines, followers | OmniSocials Python SDK: Not available | Tweepy + X API: Available, reads are billed |
| Other platforms | OmniSocials Python SDK: 12, same call | Tweepy + X API: X only |
Pick tweepy and X's API when you need to read data: search, other people's timelines, follower lists, or a bot that reacts to mentions in real time. OmniSocials doesn't cover those.
Pick the OmniSocials SDK when your job is publishing: scheduled tweets, threads, images and video, often on more platforms than X. You skip the developer app, and plain posts don't cost extra.
Complete Script: Schedule a Week of Tweets
Copy-paste ready. It schedules one tweet a day at 9:00 in New York, starting tomorrow:
"""
Schedule a week of X posts with the OmniSocials Python SDK.
https://omnisocials.com/sdk/python
Install: pip install omnisocials
Setup: export OMNISOCIALS_API_KEY="omsk_live_..."
"""
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from omnisocials import OmniSocials, ValidationError
TWEETS = [
"Write the hook last. It's easier once you know the point.",
"Most threads are too long. Cut the middle tweet and see if anyone misses it.",
"Reply to every comment in the first hour. It's the cheapest reach you'll get.",
"Post your best idea twice, a month apart. Most people missed it the first time.",
"What's one post you wish you had written this week?",
]
client = OmniSocials() # reads OMNISOCIALS_API_KEY
new_york = ZoneInfo("America/New_York")
first_slot = (datetime.now(new_york) + timedelta(days=1)).replace(
hour=9, minute=0, second=0, microsecond=0
)
for day, text in enumerate(TWEETS):
publish_at = first_slot + timedelta(days=day)
try:
post = client.posts.create(
content=text,
channels=["x"],
scheduled_at=publish_at.isoformat(),
)["data"]
print(f"{publish_at:%a %d %b %H:%M} #{post['id']} {post['status']}")
except ValidationError as err:
print(f"Skipped: {err.code}: {err.message}")
Running inside asyncio, like a bot or a FastAPI app? Swap in AsyncOmniSocials and await the same calls:
import asyncio
from omnisocials import AsyncOmniSocials
async def main():
async with AsyncOmniSocials() as client:
await client.posts.create(content="Posted from asyncio.", channels=["x"])
asyncio.run(main())
That example saves a draft, since it has no scheduled_at.
Going Further
The OmniSocials Python SDK page has the install steps and every method. The same API has official clients for Node.js, Go, Rust, .NET, Java, PHP and Ruby, all on the OmniSocials SDK overview.
Posting to more than X? The social media API in Python guide covers Instagram, LinkedIn and Bluesky with the same client. And if you'd rather schedule tweets without writing code, see our Twitter/X post scheduler comparison.
Get your API key. Sign up for OmniSocials. $10 per seat per month billed annually ($12 monthly), 12 platforms, API access included, with a 14-day free trial.
Frequently Asked Questions
How do I use the Twitter API with Python?
You have two options. Call X's own API with a library like tweepy, which needs an X developer app, four OAuth keys and tokens, and prepaid X API credits. Or use the OmniSocials Python SDK: pip install omnisocials, connect X once in the dashboard, set OMNISOCIALS_API_KEY, and call client.posts.create(content=..., channels=["x"]).
Is the Twitter API free?
No. X's API is pay-per-use: you buy credits up front and every request draws them down, with no subscription. At the time of writing, creating a post costs $0.015 and a post with a link costs $0.20, per X's pricing page. Through OmniSocials, plain X posts are included in the plan and posts with a link use 20 credits each, X's own rate passed through at cost.
Can I post to Twitter with Python without a developer account?
Yes. With OmniSocials you connect your X account once in the dashboard with a normal X sign-in. OmniSocials runs the X developer app, so you only need an OmniSocials API key and the Python SDK. You never create an app or manage OAuth tokens yourself.
What is the best Python library for the Twitter API?
Tweepy is the most widely used library for X's own API, and it's the right pick if you need to read data like search results or follower lists. If your goal is posting and scheduling, the official OmniSocials Python SDK is less work: one API key, built-in scheduling and threads, and the same call reaches 11 other platforms.
How do I schedule a tweet with Python?
X's own API has no scheduling endpoint, so with tweepy you need your own cron job or queue. With the OmniSocials SDK you pass a future ISO 8601 time as scheduled_at in client.posts.create(), and OmniSocials publishes the post at that time. A time in the past is rejected with a 400 error.
Can I post a Twitter thread with Python?
Yes. With the OmniSocials SDK, pass x={"thread_parts": [...]} with 2 to 25 parts to client.posts.create(). Each part has its own text and up to 4 media items, and OmniSocials posts them as a chained thread. With tweepy you post each tweet separately and reply to the previous one.
Sources
- OmniSocials Python SDK: Install, quickstart, and the full method list
- OmniSocials API Documentation: Full API reference, including the X options
- X API pricing: X's pay-per-use rates for creating posts
- Tweepy Documentation: The Python library for X's own API
- OmniSocials X integration guide: Connecting X, character and media limits, credits
Try OmniSocials for 14 days
Plan, schedule and answer on 12 platforms from one calendar, or let your AI agent do it. The first charge comes when the trial ends.


