status200uploads
Publish and schedule posts to TikTok, Instagram, Facebook, YouTube, X, LinkedIn, Pinterest, Threads and Skool from Python with Status 200 Uploads.
This package calls the Status 200 Uploads API (https://status200uploads.com/api/v2, described in
OpenAPI 3.1) with your API key. It is a typed client
generated from that file, plus a small hand-written layer that makes posting safe to retry: every post
carries an Idempotency-Key, and the client waits and sends again only what the API says may be sent
again.
- Install
- Your API key
- Quick start
- Media, TikTok and the other networks
- Scheduling
- What publish() returns
- When something is refused
- Safe retries
- Waits and limits
- Reading your posts
- Typed models and raw operations
- Resources
Install
pip install status200uploads
Python 3.11 or newer. It depends only on httpx and attrs.
Your API key
- In your Status 200 Uploads dashboard, open API and create an API key (it starts with
rl_). - Put it in the
STATUS200_API_KEYenvironment variable, or pass it asStatus200(api_key=...).
The key is sent only to the API, as Authorization: Bearer <key>. The client never prints it: not in
its repr, its logs or its errors. Anyone with the key can post as you, so keep it out of your code and
your repository. More in the authentication docs.
Quick start
from status200uploads import Status200
s200 = Status200() # the key comes from STATUS200_API_KEY
for account in s200.list_accounts():
print(account["handle"], [network["platform"] for network in account["networks"]])
result = s200.publish(
{
"accountId": "@myprofile",
"platform": "linkedin",
"content": {"text": "Hello from Python"},
},
wait=True,
)
final = result.final or {} # None when the post could not be read back
print(final.get("status"), final.get("permalink"))
A post goes to one network from one profile. accountId is a profile's handle (@name) or its
profile_id from list_accounts(). With wait=True the client reads the post every 30 seconds until
the network is done (for up to 10 minutes) and puts the last read in result.final. If the post cannot
be read back, result.final stays None and nothing is raised: the post itself was accepted.
Use with Status200() as s200: (or s200.close()) to close the connection when you are done.
Media, TikTok and the other networks
Media is sent by public URL: either straight in the post (content.mediaUrls), or imported first and
sent by its file_id (content.mediaID). Importing first suits videos, since the import can take
longer than one request.
media = s200.import_media("https://example.com/video.mp4") # waits until it is ready
profile = s200.list_accounts()[0]
options = s200.get_posting_options(profile["profile_id"], platforms=["tiktok"])
tiktok = options["networks"][0]
print(tiktok["options"]["privacy_level_options"]) # the levels this TikTok account allows
result = s200.publish(
{
"accountId": profile["profile_id"],
"platform": "tiktok",
"content": {"text": "New video", "mediaID": [media["file_id"]]},
"tiktok": {"privacyLevel": "SELF_ONLY"},
}
)
| Network | Needs | Where |
|---|---|---|
| TikTok | a privacy level the account allows, and media | post.tiktok.privacyLevel (see get_posting_options()) |
| YouTube | a video; its privacy | post.youtube.privacyStatus (and title, tags ...) |
| a board and an image | post.pinterest.boardId (boards from get_posting_options()) |
|
| Skool | a group and a title | post.skool.group, post.skool.title |
| media | post.instagram.postType (feed image, Reel, story, carousel) |
|
| X, LinkedIn, Threads, Facebook | nothing more | text posts need no media |
Every field is in the OpenAPI file
(components.schemas.Post) and the API docs. A field the API
does not use never refuses a post: it comes back in result.warnings.
Check a post first. s200.validate(post) runs every check a publish makes and sends nothing. The
report says outcome (publish, schedule, queue or refuse), would_publish, reason and each check.
Scheduling
from datetime import datetime, timezone
post = {"accountId": "@myprofile", "platform": "x", "content": {"text": "See you on Thursday"}}
result = s200.publish(post, scheduled_for=datetime(2026, 10, 1, 9, 0, tzinfo=timezone.utc))
print(result.code) # "scheduled"
s200.cancel(result.status200.scheduled_post_id) # stop it before it goes out
scheduled_for takes an ISO 8601 time with a time zone ("2026-10-01T09:00:00Z"), a Unix time, or a
datetime that has a time zone. A datetime without one is refused before anything is sent, because
the API refuses such a time. More than 60 seconds ahead (up to 365 days) the post is scheduled; your
plan's allowance is checked when it goes out.
What publish() returns
A Result for every answer that is a success:
result.status_code |
200: published (or a dry run's report). 202: accepted, not published yet |
result.code |
the 202's case: scheduled, queued_for_next_day (daily allowance used up), still_publishing (the network is slower than the request), or None while data.status is processing |
result.status200 |
our ids: post_id, scheduled_post_id and status_url (read it with get_post() or wait_for()) |
result.data |
the network's own answer (its fields differ per network) |
result.warnings |
fields that were not used, and other things to know |
result.replayed |
True when this is the stored first answer to the same Idempotency-Key |
result.idempotency_key |
the key the post was sent with |
result.raw |
the whole JSON answer |
A 202 is never a failure: the post is on its way, scheduled or queued, and it must not be sent again.
TikTok and Instagram finish after the answer (data.status: "processing"); wait_for(result) follows
them to the end.
When something is refused
from status200uploads import OutcomeUnknown, Status200Error
try:
s200.publish(post)
except OutcomeUnknown as error:
# No clear answer: the post may have gone through.
print(error.message)
except Status200Error as error:
print(error.code, error.message) # e.g. monthly_limit_reached, and what to do
print(error.details) # the refusal's facts: resets_at, profiles, upgrade_url ...
Status200Error carries the API's code (branch on it), its message (a sentence for a person), the
HTTP status, the refusal's facts in details, retry_after_seconds when the API asked for a wait,
reason, which says why the client stopped (refused, max_wait, tries_used, no_key,
once_used or import_failed), and note, what the client did before it stopped. str(error) puts
the message, the note, the status and the code together.
OutcomeUnknown (a Status200Error) means the client cannot tell whether the post went through: no
answer arrived, or a page that is not JSON, or a server error, even after sending it once more. Look at
your posts (s200.list_posts() or History in the dashboard) before sending it again, or send it again
with the same idempotency_key within 24 hours, which never posts twice.
Safe retries
Every publish() and import_media() carries an Idempotency-Key. The API answers the same key with
the same request, within 24 hours, with its first answer (result.replayed is then True) instead of
doing it twice.
- Automatic (the default): a new key for each call, reused on every try the client makes, with the exact same body.
- Your own key:
s200.publish(post, idempotency_key=f"{row_id}-linkedin"), made from your record and the network. The same record then never posts twice, even from another run of your program. Build it from something stable, never from the time or a random value. A different post under the same key is refused (idempotency_key_reused). - None:
idempotency_key=False. An answer whose outcome is not known is then never sent again.
Waits and limits
The client follows the API's retry table (components.x-status200-retry in the
OpenAPI file):
| Answer | What the client does |
|---|---|
409 media_processing (a file still importing), 409 idempotency_in_progress, 429 rate_limited |
Waits as long as the API asks (Retry-After), then sends the identical request again, within max_wait |
503 "nothing was sent" answers, 502 upstream_unavailable, 504 cancel_unconfirmed |
Waits and sends again, at most twice |
An answer whose outcome may not be known: a 5xx such as server_error or platform_error, a page that is not JSON, or no answer at all |
Sends it once more, and only with an Idempotency-Key (a read, a cancel or a dry run always); otherwise raises OutcomeUnknown |
Any 202 |
A success: never sent again |
| Every other refusal | Raises Status200Error at once |
Status200(max_wait=120, max_tries=5, timeout=httpx.Timeout(120, connect=10)) are the defaults:
max_wait is the most the client waits in all when the API asks it to (seconds; 0 never waits, and
the error then says how long to wait), max_tries the most requests for one call. Each call of
publish() can override max_wait and max_tries.
The API's limits: one post per network every 20 seconds per account, one media import every 20
seconds, 60 reads a minute per account (wait_for() reads every 30 seconds), and 10 posting-option
reads a minute. Your plan's daily and monthly allowances apply; a post over the daily allowance is
queued for the next day (202 queued_for_next_day).
The client logs each wait at INFO on the status200uploads logger (never the key).
Reading your posts
page = s200.list_posts(limit=50, status=["failed"], platform="tiktok")
for post in s200.iter_posts(platform="x"): # every page, newest first
print(post["id"], post["status"], post.get("permalink"))
post = s200.get_post(result.status200.post_id)
final = s200.wait_for(result) # every 30 seconds, for up to 10 minutes
Typed models and raw operations
The request and answer objects of the OpenAPI file are in status200uploads.models (attrs classes
with to_dict() and from_dict()), and publish() takes them as well as dicts:
from status200uploads.models import Post, PostContent
s200.publish(Post(account_id="@myprofile", platform="x", content=PostContent(text="Typed")))
The raw operations are in status200uploads.api (posts, accounts, media), each with sync(),
sync_detailed(), asyncio() and asyncio_detailed(). Pass them s200.client, which shares the key,
timeout and connection. They send each request once, exactly as given: no Idempotency-Key unless you
pass one, no waits and no retries.
from status200uploads.api.accounts import list_accounts
answer = list_accounts.sync(client=s200.client)
Resources
- Status 200 Uploads API documentation, and its Python guide
- OpenAPI 3.1 description
- Support: https://status200uploads.com/support or info@status200uploads.com
- Source and issues: https://github.com/iBoyDroid/status-200-uploads (folder
python); security reports go to info@status200uploads.com, see SECURITY.md - Changelog
Working on this package
The code under src/status200uploads/_generated is generated from openapi/openapi.yaml and never
edited by hand:
pip install -r python/tools/requirements.txt
bash scripts/regenerate-python.sh
pip install -e "python[test]"
pytest python
License
Metadata
Release files for status200uploads 0.1.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 | |
|---|---|---|---|
| status200uploads-0.1.0.tar.gz | 64.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| status200uploads-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 197.2 kB
Release files / status200uploads-0.1.0.tar.gz
| Download URL | status200uploads-0.1.0.tar.gz |
|---|---|
| Size | 64.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
09ec080da303b32eed6f6dfa0e2f633a217ec63cba03f9012958c77304c7dc7a
|
|
BLAKE2b-256 checksum How to use checksums |
42b6e6fc108d5cb9ced2906923d9a32af6ba2a062191b2cb4c74ce3257795936
|
| 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 29, 2026.
Transparency logRelease files / status200uploads-0.1.0-py3-none-any.whl
| Download URL | status200uploads-0.1.0-py3-none-any.whl |
|---|---|
| Size | 132.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
68cf2a51b50b94b4c00d3afe1a9912daa7a11bbca6b8a58906413218e657a99f
|
|
BLAKE2b-256 checksum How to use checksums |
1d195dcf0260005ce92c31bedd1dc77787e2f7d85221aad66696d36ba2b6a05d
|
| 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 29, 2026.
Transparency log