truthsocial-py
An unofficial, typed Python client for Truth Social.
truthsocial-py supports OAuth app discovery, manual app configuration, multiple
independent user sessions, text and media posts, replies, and structured API
errors.
[!NOTE] Truth Social does not publish this interface as a stable public API. Endpoints and payloads may change.
Features
- Discover the OAuth app identity from the deployed Truth Social web client
- Configure an OAuth app identity manually
- Log in multiple users with isolated tokens, HTTP state, and session IDs
- Rediscover rotated web-app credentials automatically or on demand
- Publish text and image posts
- Reply to a status by ID or
Statusobject - Answer new-device security-code challenges over email or SMS
- Complete 2FA logins with an authenticator (TOTP) code
- Reuse existing access tokens
- Handle authentication, rate-limit, transport, and protocol errors
Installation
Python 3.10 or newer is required.
python -m pip install truthsocial-py
The distribution is truthsocial-py; the import package is truthsocial_py:
from truthsocial_py import TruthSocialApp
To work on the library itself:
git clone https://github.com/Jxck-S/truthsocial-py.git
cd truthsocial-py
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e .
Quick start
Create an app from the current Truth Social web deployment, then log in:
import getpass
from truthsocial_py import TruthSocialApp
app = TruthSocialApp.from_web()
with app.login(
username=input("Truth Social username: "),
password=getpass.getpass("Truth Social password: "),
) as client:
account = client.verify_credentials()
status = client.post_status("Hello from truthsocial-py!")
print(f"Posted as @{account.acct}: {status.url}")
App identities
TruthSocialApp represents one OAuth application identity. Load the current
identity from the website:
app = TruthSocialApp.from_web()
Or provide one manually:
app = TruthSocialApp(
client_id="your-client-id",
client_secret="your-client-secret",
)
Web-loaded apps enable automatic rediscovery by default. If login returns
OAuth's invalid_client or unauthorized_client error, the app refreshes its
identity and retries once. Refresh it explicitly at any time:
credentials = app.rediscover()
Rediscovery applies to future clients and login attempts. Existing clients
keep their current token and app snapshot. A manually configured app can opt
in with auto_rediscover=True.
Multiple users
Every login returns a separate TruthSocialClient:
alice = app.login("alice", "alice-password")
bob = app.login("bob", "bob-password")
try:
alice.post_status("Posted by Alice")
bob.post_status("Posted by Bob")
finally:
alice.close()
bob.close()
Create a client from a previously saved token without logging in again:
alice = app.new_client(access_token=load_alice_token())
User agent
Requests are sent with truthsocial-py/<version> by default, exposed as
truthsocial_py.DEFAULT_USER_AGENT. Override it on an app or a client:
from truthsocial_py import DEFAULT_USER_AGENT, TruthSocialApp
app = TruthSocialApp.from_web(user_agent="my-bot/2.0 (+https://example.com)")
client = app.new_client() # inherits the app's user agent
print(client.user_agent)
TruthSocialClient(..., user_agent=...) works the same way. The value must be
a non-empty, header-safe string; anything else raises ConfigurationError.
Media posts
Pass paths through media_files; each file is uploaded before the status:
status = client.post_status(
"A photo post",
media_files=["photo.png"],
idempotency_key="your-stable-unique-key",
)
for attachment in status.media_attachments:
print(attachment.id, attachment.url)
Upload separately when you need the attachment first:
attachment = client.upload_media("photo.png")
status = client.post_status(
"Uploaded separately",
media_ids=[attachment.id],
)
upload_media() also accepts open binary files with optional filename and
content_type arguments. Do not combine media_files and media_ids in the
same call. Media types and size limits are controlled by Truth Social.
Truth Social currently accepts public visibility for these posting calls.
Replies
Reply using a parent status ID:
reply = client.reply("parent-status-id", "This is a reply")
Or reply to a returned Status, including media:
reply_with_photo = client.reply(
reply,
"Replying to my reply",
media_files=["photo.png"],
)
The lower-level equivalent is
post_status(..., in_reply_to_id="parent-status-id").
Low-level client
TruthSocialClient can be used directly:
from truthsocial_py import TruthSocialClient
with TruthSocialClient(
client_id="your-client-id",
client_secret="your-client-secret",
) as client:
client.login("username", "password")
It can also discover the deployed app identity for a single login:
with TruthSocialClient() as client:
client.login_with_web_app("username", "password")
Use discover_web_app_credentials() to obtain the typed app identity without
logging in.
New device verification
Logging in from a device Truth Social has not seen before is rejected with a
DeviceChallengeRequired — a subclass of AuthenticationError, so existing
handlers still catch it, but it is raised only when the username and password
were accepted. Answering it takes two more calls: pick a delivery method to
have a 6-digit code sent, then repeat the login with that code.
Own the client yourself so the challenge is answered on the session that raised it (see Challenge handling below):
from truthsocial_py import DeviceChallengeRequired, TruthSocialApp
app = TruthSocialApp.from_web()
username, password = "someone", "hunter2"
with app.new_client() as client:
try:
client.login(username, password)
except DeviceChallengeRequired as exc:
challenge = exc.challenge
print(exc.message) # "New device login detected. Please select a..."
for option in challenge.delivery_options:
print(option.kind, option.value) # e.g. email j***@example.com
client.send_security_code(challenge, "email")
client.login_with_security_code(
username,
password,
security_code=input("security code: "),
challenge=challenge,
)
client.post_status("hello from a verified device")
send_security_code may be called again with the same challenge to resend the
code. The password is required a second time because
/oauth/v2/verify_security_code issues the token itself; the challenge is not
a token exchange. TruthSocialApp.login never retries credential rediscovery
on this error, since the app credentials were not the problem.
Two-factor accounts
An account with 2FA enabled rejects the password grant with MfaRequired,
another AuthenticationError subclass, handing back a short-lived mfa_token.
That token stands in for the password — redeeming it with the authenticator
code returns the access token directly.
from truthsocial_py import MfaRequired, TruthSocialApp
app = TruthSocialApp.from_web()
with app.new_client() as client:
try:
client.login("someone", "hunter2")
except MfaRequired as exc:
client.login_with_mfa_code(input("2FA code: "), challenge=exc.challenge)
client.post_status("hello from a 2FA account")
login_with_mfa_code defaults to challenge_type="totp", the only type Truth
Social currently advertises. Note that the 403 body claims "The 2FA code
entered is incorrect" even on the first prompt, before any code has been sent;
that sentence is kept on exc.challenge.detail rather than used as the
exception message, so it cannot be mistaken for a rejected code. A code that
really is wrong fails the login_with_mfa_code call with a plain
AuthenticationError carrying that same message.
Challenge handling
Both challenge flows are answered on the same client that raised them.
Each client carries its own X-Truth-Session-Id and connection, and a
challenge belongs to that session, so create the client first and keep it in
scope for the handler:
with app.new_client() as client: # you own the client
try:
client.login(username, password)
except MfaRequired as exc:
client.login_with_mfa_code(code, challenge=exc.challenge)
TruthSocialApp.login creates a client internally and closes it when login
fails, so it cannot be used to answer a challenge — it is the one-shot path
for accounts with 2FA disabled logging in from a known device. Owning the
client trades away its automatic retry on rotated app credentials; handle that
by calling app.rediscover() yourself if a login fails with an
invalid_client error.
An account can require both: /oauth/v2/verify_security_code may itself
return mfa_required, so the two handlers compose on the one client.
Posting behavior
Status creation is not retried automatically because a transport failure can
leave the final outcome unknown. Supply a stable idempotency_key when your
application may retry a post.
When media_files is used, a later upload or post failure can leave an
uploaded attachment unused.
Errors
All library exceptions inherit from TruthSocialError:
ConfigurationErrorandNotAuthenticatedErrorCredentialDiscoveryErrorAuthenticationError, and itsDeviceChallengeRequiredandMfaRequiredsubclassesRateLimitError, including an optionalretry_afterAPIErrorNetworkErrorandProtocolError
Development
Run the mocked test suite:
PYTHONPATH=src python -m unittest discover -s tests -v
Live smoke test
Prepare the local settings file:
cp examples/local_credentials.py.example examples/local_credentials.py
Fill in the username and password, then run:
python examples/manual_smoke_test.py
After confirmation, the script creates a public text post, a public reply, and
a public image post using examples/truthsocial-py-test.png. It does not delete them
afterward.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters