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.
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_afterForbiddenError— a 403 with no JSON bodyAPIErrorNetworkErrorandProtocolError
ForbiddenError vs AuthenticationError
Truth Social explains every application-level refusal in a JSON body. A 403
that arrives with no body at all — no error, no detail, no x-request-id
header — did not come from the application; it came from the edge in front of
it, which scores requests for bot/WAF signals and drops a fraction of them.
/api/v1/media is by far the most common victim, and uploads can fail this way
at a steady single-digit-to-20% rate while the same token posts to
/api/v1/statuses without a single failure.
These raise ForbiddenError, which is not an AuthenticationError. That
distinction matters: the access token is untouched and still valid, so
re-authenticating cannot help, throws away a working session, and on a 2FA
account spends a TOTP code — fast enough to collide with the previous code's
30-second window and lock the account out of its own retry.
Treat it as a transient refusal of that one request: back off for minutes rather than seconds (an immediate retry usually hits the same block), or degrade gracefully.
from truthsocial_py import ForbiddenError
try:
status = client.post_status("hello", media_files=["map.png"])
except ForbiddenError:
# The upload was blocked, not the session. Post without the image.
status = client.post_status("hello")
Anything that does explain itself keeps the old behaviour: a 403 carrying an
error code or a detail string, and any 401, still raise
AuthenticationError, and mfa_required / security_code_required still take
priority as MfaRequired / DeviceChallengeRequired.
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.
Metadata
Release files for truthsocial-py 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| truthsocial_py-0.3.0.tar.gz | 33.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| truthsocial_py-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.0 kB