Skip to main content

truthsocial-py

truthsocial-py logo

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 Status object
  • 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:

  • ConfigurationError and NotAuthenticatedError
  • CredentialDiscoveryError
  • AuthenticationError, and its DeviceChallengeRequired and MfaRequired subclasses
  • RateLimitError, including an optional retry_after
  • ForbiddenError — a 403 with no JSON body
  • APIError
  • NetworkError and ProtocolError

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)

Source distribution for truthsocial-py 0.3.0
File Size Uploaded
truthsocial_py-0.3.0.tar.gz 33.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for truthsocial-py 0.3.0
File Interpreter ABI Platform
truthsocial_py-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 56.0 kB

Release files / truthsocial_py-0.3.0.tar.gz

Download URL truthsocial_py-0.3.0.tar.gz
Size 33.3 kB
Tags Source
SHA-256 checksum
How to use checksums
d57bc60151f189d6bbc9ade5285a40615cd9a2c63b00a8a0fc65b48bde08b078
BLAKE2b-256 checksum
How to use checksums
32c9108bbffcbbd7fe37f334ed3e4330ed92fe4b04db58c0364c5658e629156a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / truthsocial_py-0.3.0-py3-none-any.whl

Download URL truthsocial_py-0.3.0-py3-none-any.whl
Size 22.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8bab36d4046361525bfdac1a2aab58f7267d0aea67ede2937113f1f2d3547daa
BLAKE2b-256 checksum
How to use checksums
b92f0e8f7e9f5be0eb32e2cd4eed6cc14e997741fedd67195faad1887a926b27
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.0

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page