API Documentation
The Status 200 Uploads API lets you upload media and publish to TikTok, Instagram, Facebook, YouTube, X (Twitter), LinkedIn, Pinterest, Threads, and Skool programmatically.
It imports a media file from a URL (/media) or takes one uploaded from a computer through a one-time link (/media/uploads, paid plans), creates a post that is published at once or scheduled for later (/posts, with post.scheduledFor), and cancels a scheduled post (DELETE /posts/{id}). It also reads back your profiles and their connected networks (GET /accounts), what each network allows (GET /accounts/{id}/options), and your posts with their status, link and figures (GET /posts, GET /posts/{id}). Connecting accounts, analytics across posts and the calendar are in the dashboard.
The API is described in OpenAPI 3.1 at https://status200uploads.com/openapi.yaml, both URLs, every answer and every error code: import it into Postman or Insomnia, or generate a client from it.
Base URL
https://status200uploads.com/api/v2
Every endpoint lives under this URL
Protocol
HTTPS
All requests must use HTTPS
Format
JSON
Request and response bodies are JSON
The older URLs https://app.status200uploads.com/functions/v1/api-posts and https://app.status200uploads.com/functions/v1/api-media-upload accept the same requests and keep working.
Supported Platforms
TikTok
Video, Photo slideshow
Image, Reel, Story, Carousel
Photo, Video, Reel, Text
YouTube
Video
X (Twitter)
Text, Image, Video
Text, Image, Video
Image Pin
Threads
Text, Image, Video, Carousel
Skool
Text, Image, Video
Authentication
All API requests require a valid API key passed in the Authorization header using Bearer token format.
Generating API Keys
- Sign in to your dashboard
- Navigate to API Management
- Click "Generate Key" and give it a descriptive name
- Copy the key immediately - it will only be shown once
Using Your API Key
Authorization: Bearer rl_your_api_key_hereA post or a media import may also carry an optional Idempotency-Key header, so that a request sent again after a timeout is never done twice (see Safe Retries).
Keep Your Keys Secure
Never expose API keys in client-side code, public repositories, or logs. Use environment variables and server-side proxies. Rotate keys periodically and revoke any that may have been compromised.
Key Rotation Best Practices
- 1. Generate a new key before revoking the old one
- 2. Update all services to use the new key
- 3. Verify traffic is flowing with the new key
- 4. Revoke the old key from your dashboard
Connect an AI app (Claude, Cursor, VS Code …)
Status 200 Uploads runs an MCP server at https://mcp.status200uploads.com/mcp. Add that address in your AI app, sign in to Status 200 Uploads on the page it opens, and choose whether the app may only read or may also post. No key is copied anywhere. Connected apps are listed under Dashboard → API, and Disconnect cuts one off at once. The AI connector (this MCP server) is included in every paid plan, from Beginner, whether an app signs in or sends an API key. On Free, use the REST API with your rl_ API keys.
An app you connected this way gets the seventeen tools below. Everything a connected app does goes through the same rules as the REST API — your plan's post allowance, the X add-on, the 20-second spacing between posts — and it can only ever see the account that signed in.
An app that can run commands on your computer (tested with Claude Code; other AI apps that can run commands may work) can also post a video or an image that is on that computer: it asks for a one-time private upload link, runs our small uploader, and the file is checked (exact size, type and SHA-256) before it can be posted. Nothing is put on a public site on the way. This is part of the paid plans, within one daily budget per account for every upload link and the URL imports of connected AI apps (see Upload from a computer); an app that cannot run commands sends you to the dashboard (Media) instead.
A file you already uploaded in the dashboard (Media) needs no link at all: tell the app its name, and it finds it in your media library and posts it by its file id. The app sees each file's name, type, size and upload time, never its address.
Looking at the account
These never change anything.
status200_list_accounts | Your profiles and the networks connected to each one, with a note when a connection needs reconnecting. |
status200_get_usage | Your plan, how much of the post allowance is used and when it resets. |
status200_get_posting_options | What this account can really do on a network: the TikTok privacy choices your creator account allows, your Pinterest boards, your Skool groups and labels, the Facebook page, how many Instagram posts are left in the last 24 hours, and the YouTube categories. A network that cannot be asked right now is named, and the rest are still answered. |
status200_get_media_status | How far an imported or uploaded file has got, and whether it is ready to post. |
status200_list_media | The files in your media library, newest first, found by name, type or upload time: a file you uploaded in the dashboard, imported from a link or sent through an upload link, with whether it can be posted now. Files are kept 7 days after upload (longer while a scheduled post uses them). |
status200_get_post | One post: which network it went to, whether it worked, the link to it, or why it failed, and what we changed to get a TikTok post out (for example a photo sent as a smaller copy). For a scheduled post: its outcome and, once it has gone out, each network's result. |
status200_list_posts | A page of your post history, newest first, with what we changed to get each TikTok post out (for example a photo sent as a smaller copy). Filter by network, status or profile. Posts waiting to go out, cancelled ones and ones that failed before anything was sent are listed too; once a scheduled post has gone out, each network's post is listed instead. |
status200_get_post_performance | Views, likes, comments, shares, impressions, reach, saves, clicks and watch time — for one post, or for a list such as "my best posts this month". It says when the numbers were last collected. Not every network reports every number, and a post nobody has collected numbers for yet has none: those stay empty rather than being shown as zero. |
Before you post
The cheapest way to stop an assistant from spending your allowance on a post that was never going to be accepted.
status200_validate_post | Runs every check that publishing would run — your allowance, the network gates, the media, the privacy choices, caption lengths, YouTube tags, the Pinterest board, the Skool group — and says what would happen. Nothing is sent, nothing is counted, nothing is recorded. |
Changing something
Only an app you allowed to post can use these. A read-only app is refused before the tool runs.
status200_upload_media_from_url | Import an image or video from a URL so it can be posted. Google Drive, Dropbox and OneDrive share links are rolling out, on for some accounts first (see Import media). |
status200_create_upload_link | Upload a video or an image from the computer the app runs commands on: a one-time private link and a ready-made command for our uploader (paid plans, within the daily budget). |
status200_finish_upload | After the upload: check the file that arrived (exact size, type and SHA-256) and put it in your media library, ready to post. |
status200_publish_post | Publish now, to up to ten places in one call (one per network). A post over your daily allowance is queued for the next day instead of failing. |
status200_schedule_post | Put a post on the calendar for a time you choose. The time has to be in the future, and the caption is checked against each network first. |
status200_update_scheduled_post | Change the caption or the time of a post that has not gone out yet. |
status200_cancel_scheduled_post | Cancel a post that has not gone out yet. |
status200_retry_post | Send a failed post again. |
Worth knowing
- TikTok needs a privacy choice and YouTube needs a privacy setting on every post. The tools ask you rather than choosing for you.
- Posts an app scheduled are shown per app under Dashboard → API. Disconnecting an app keeps them unless you ask for them to be cancelled; posts you scheduled yourself are never touched.
- Once a post is out, Status 200 Uploads cannot delete it from the network.
- Numbers are collected periodically, not live, which is why every answer says when they were last collected.
Or connect with an API key
An MCP client that cannot sign in can send an rl_ key instead. That path needs a paid plan too (the key of a Free account works with the REST API only) and offers the older, smaller set of tools (server info, create a post, import media). The Integrations hub has a snippet per client, and https://mcp.status200uploads.com/health says whether the server is up.
{
"mcpServers": {
"status200-uploads": {
"url": "https://mcp.status200uploads.com/mcp",
"headers": {
"Authorization": "Bearer rl_put_your_dashboard_key_here"
}
}
}
}Running your own copy of the server? Keep SUPABASE_SERVICE_ROLE_KEY on that machine only — never in an agent, a repository or a browser bundle. The operational detail is in mcp-server/README.md.
Rate Limits
Your plan sets a post allowance: successful posts, all platforms combined. Free: 30 per account per calendar month (UTC), for every Free account from 18 October 2026. Posts published from 1 October 2026 count toward October's 30; if 30 or more are already used on 18 October 2026, posting pauses until 1 November 2026. Until 18 October 2026, Free accounts created since 18 September 2026 have 60 per calendar month and older ones 5 per profile per UTC day. Beginner and Apprentice: per profile per UTC day (00:00 to 24:00 UTC). Skilled and up: no allowance. Failed posts do not count. The same allowance applies to the dashboard, the scheduler and the API. Posts to X use the X add-on's own allowance (200 per billing month, up to 20 with links), not the plan's.
Over a daily allowance a request is answered HTTP 202 queued_for_next_day and the post is queued for the same time on the next day; over the Free monthly allowance it is answered 429 monthly_limit_reached (see the Create a post reference for the responses). A post scheduled with post.scheduledFor is checked against the allowance when it goes out, not when it is scheduled.
Post Allowance by Plan
| Parameter | Type | Description |
|---|---|---|
Free | 30 posts per month | Successful posts per account per calendar month (UTC), all platforms combined |
Beginner | 20 posts per day per profile | Successful posts per profile per UTC day, all platforms combined |
Apprentice | 50 posts per day per profile | Successful posts per profile per UTC day, all platforms combined |
Skilled | Unlimited posts | No post allowance (publish attempts are capped at 500 per profile per day) |
Expert | Unlimited posts | No post allowance (publish attempts are capped at 500 per profile per day) |
Agency | Unlimited posts | No post allowance (publish attempts are capped at 500 per profile per day) |
Request Throttle
Apart from the post allowance, requests are spaced per account (all its profiles together): one post per platform every 20 seconds (a scheduled post takes no such wait, and neither does a request answered again from its Idempotency-Key) and one media import every 20 seconds. A request that comes sooner is answered with HTTP 429, and the message says how many seconds to wait. The same seconds are in a Retry-After header and in the body as retry_after_seconds (inside error for a post, next to it for a media import), for tools that read only the body, such as n8n. Every other 429 carries retry_after_seconds too. Status requests for a media import are not throttled. There are no other rate limit headers.
Read Limit
The reads (GET /api/v2/accounts, /accounts/{id}/options, /posts and /posts/{id}) are limited to 60 a minute per account (all your API keys together), and posting options to 10 a minute per account on top, because each of those asks the networks themselves with your own sign-in. Over a limit the answer is HTTP 429 rate_limited with a Retry-After header and, in the body, error.retry_after_seconds and error.limit (60 or 10). The limit counts per UTC minute. Reads are not counted in your API key's request count (a key used only for reads shows 0 requests on the API page), and a read never uses the post allowance. To wait for a post, one read every 30 seconds, for up to 10 minutes, is plenty. A refused read is written to your API request log once a minute per endpoint and status, however often it is sent.
Publish Attempts
Failed posts do not use the post allowance, but publish attempts per profile per UTC day (successful and failed together) are capped at three times the daily allowance plus 20 (Beginner 80), 80 per profile per day on Free, and 500 per profile per day on plans without an allowance. Past that, requests are answered with HTTP 429 daily_attempts_exceeded until 00:00 UTC.
Safe Retries (Idempotency-Key)
A request can time out after the post already went out. Send an Idempotency-Key header with a value that names this one request (a UUID, or something like order-42-tiktok), and send it again with the same key and the same body: if the first request got an answer within the last 24 hours, you get that answer back, byte for byte, with the header Idempotent-Replayed: true, and nothing is posted or imported a second time.
- - Optional, on POST only, at four addresses:
/api/v2/posts,/api/v2/media, and the olderhttps://app.status200uploads.com/functions/v1/api-postsandhttps://app.status200uploads.com/functions/v1/api-media-upload. A request without the header works exactly as before. GET, DELETE and dry runs ignore it. - - 1 to 255 printable ASCII characters (letters, digits, spaces and punctuation). Spaces around it and one pair of double quotes are removed. An empty value or anything else is refused with 400
idempotency_key_invalidand nothing is sent. - - A key belongs to the API key that sent it, and posts and media are kept apart: the same value can name a media import and the post that uses it. The two posts addresses share one entry (a key sent to one and then the other is answered again at the second), and so do the two media addresses. A key is remembered for 24 hours from the first request; after that it is new again.
- - The same request means the same JSON body (the order of its fields does not matter). The same key with a different body is refused with 422
idempotency_key_reused(first_used_atsays when the key was first used), and nothing is sent: use a new key for each new request. - - While the first request is still being worked on, the same key gets 409
idempotency_in_progresswithRetry-After: 5(andretry_after_secondsin the body): send it again then to get its answer. This is a different code frommedia_processing. - - A first request that never finished (more than 10 minutes without an answer) gets 409
idempotency_outcome_unknownand noRetry-After: for a post, checkGET /api/v2/postsor History, and if it is not there send it with a new key; for a media import, send it again with a new key (a second import does no harm). - - When the key cannot be checked just now, the answer is 503
idempotency_unavailablewithRetry-After: 5, and nothing is sent: send the same request with the same key again. Rarely, that key is then answered 409idempotency_in_progressfor up to 10 minutes andidempotency_outcome_unknownafter. If every earlier try with that key got an answer (none timed out) and each was a 503idempotency_unavailable, nothing was sent: send it with a new key.
What is remembered
- - Remembered: every 2xx answer (published, processing,
still_publishing,scheduled, queued for the next day; for media a 200 or 202 with itsfile_id), and an answer whose outcome we could not confirm (the post may have gone out), such as a 5xx that names no cause (platform_erroron/api/v2/posts) after the post was handed to the network. Those are answered again for 24 hours: check History, and send the post again with a new key only if it is not there. - - Not remembered: refusals (400 to 429,
media_processing, the 503s before sending) and failures the network reported with their own code (History shows them as failed). Fix what the message names and send it again with the same key: it is sent afresh, so a retry loop sends such a failure to the network again on every try. - - An answer sent again is the first answer, not the current state: a replayed 202
still_publishing,processingorscheduledsays what happened then. Readstatus200.status_url(orGET /api/v2/media?file_id=for an import) for the outcome. To send a post again after it failed in the background, use a new key. - - An answer sent again takes no 20-second wait and counts as a request in your API key's request count (it is in your request log); it never uses the post allowance.
curl -i -X POST 'https://status200uploads.com/api/v2/posts' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer rl_your_api_key' \
-H 'Idempotency-Key: 8e03978e-40d5-43e8-bc93-6894a57f9324' \
-d '{"post": {"accountId": "@yourprofile", "platform": "x", "content": {"text": "Hello"}}}'
# The same command again: the same answer, with "Idempotent-Replayed: true", and one post.Retry automatically only after a timeout, a lost connection, a 5xx without our error.code (a gateway's page), and the codes in RETRY_CODES above, each after its Retry-After: nothing was sent then, or the first answer comes back. After any other 5xx, read the message and check History first. In n8n, Make and Zapier, build the key from the item and the network (for example {{ $json.id }}-tiktok, with an id that is never empty), never from the time or a random value, so that a retry sends the same key, and keep the body the same on every try (see the Guides).
Quick Start
Get up and running in two steps: upload media, then publish a post.
Upload Media
curl -X POST 'https://status200uploads.com/api/v2/media' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer rl_your_api_key' \
-d '{"url": "https://example.com/video.mp4"}'{
"success": true,
"file_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"size": 5242880,
"type": "video/mp4",
"status": "ready"
}A large file answers 202 with "status": "processing" and is imported in the background. Request GET https://status200uploads.com/api/v2/media?file_id=YOUR_FILE_ID every 10 to 30 seconds until the status is "ready" before you publish (see the Import media reference). A file on your computer rather than at a URL: see Upload from a computer (paid plans).
Publish a Post
curl -X POST 'https://status200uploads.com/api/v2/posts' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer rl_your_api_key' \
-d '{
"post": {
"accountId": "@yourprofile",
"platform": "tiktok",
"content": {
"text": "My first API post! #automation",
"mediaID": ["YOUR_FILE_ID"]
},
"tiktok": {
"privacyLevel": "PUBLIC_TO_EVERYONE"
}
}
}'{
"data": {
"status": "processing",
"post_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"publish_id": "v_pub_url~v2-1.7123456789012345678",
"message": "Post is being processed. Check post history for updates."
},
"status200": {
"post_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"scheduled_post_id": null,
"status_url": "https://status200uploads.com/api/v2/posts/b2c3d4e5-f6a7-8901-bcde-f12345678901"
}
}The body is the network's result inside data, and status200.status_url reads the post back (GET /api/v2/posts/{id}). TikTok finishes processing the video after the response: the final result is shown on the History page of your dashboard. Every answer arrives within about 25 seconds; a 202 with "code": "still_publishing" means the network is still working on it, so do not send the request again without the same Idempotency-Key (see Safe Retries).
API Reference
/api/v2/postsCreate, publish or schedule a post. Each request goes to one network from one profile.
Full URL: POST https://status200uploads.com/api/v2/posts. The older URL POST https://app.status200uploads.com/functions/v1/api-posts accepts the same request and keeps working (its few differences are listed at the end of this section). A post that has not gone out yet can be cancelled with DELETE https://status200uploads.com/api/v2/posts/{id}. Every answer that made a post or a scheduled post names it in a status200 field, and GET https://status200uploads.com/api/v2/posts/{id} reads it back: its status, its link on the network and its figures. GET https://status200uploads.com/api/v2/posts lists your posts and your waiting posts, and GET https://status200uploads.com/api/v2/accounts your profiles (see List profiles, Posting options, List posts and Read a post, below).
The whole API, at both URLs, with every answer and every error code, is described in OpenAPI 3.1 at https://status200uploads.com/openapi.yaml: import it into Postman or Insomnia, or generate a client from it.
To publish later, send post.scheduledFor with a time zone: the post is scheduled for that time and the answer is 202 scheduled (see Schedule and cancel, below). A time in the past or within the next 60 seconds publishes at once.
Besides scheduledFor, a post is deferred in one case: when a Beginner or Apprentice profile has used its daily allowance (successful posts per profile per UTC day, all networks combined, see Rate Limits), the request is not refused. The server answers 202 Accepted and queues the post for the next day. On the Free plan (30 successful posts per calendar month per account) a post over the allowance is refused with 429 monthly_limit_reached. Skilled, Expert and Agency have no allowance (publish attempts are capped at 500 per profile per UTC day).
Request Headers
| Parameter | Type | Description |
|---|---|---|
Content-Typerequired | string | application/json |
Authorizationrequired | string | Bearer rl_your_api_key |
Idempotency-Key | string | Optional. 1 to 255 printable ASCII characters naming this one request (a UUID, for example). The same key with the same body within 24 hours gets the first answer again, with Idempotent-Replayed: true, and nothing is posted twice. The two posts URLs share keys. See Safe Retries |
Body Parameters
| Parameter | Type | Description |
|---|---|---|
post.accountIdrequired | string | Profile to post from, as shown on your Connections page: the profile UUID, "@handle" or the bare profile name (e.g. "@myprofile"). Capital letters do not matter: "@MyShop" finds the profile "myshop". If two of your profiles differ only in capital letters, send the exact name or the UUID. A profile that is not on your account is refused with 403 account_not_found, and error.profiles lists your profiles |
post.platformrequired | string | Target network: tiktok, instagram, facebook, youtube, x, linkedin, pinterest, threads, skool |
post.content.text | string | Caption, title or post text. Required when the post has no media |
post.content.mediaID | string[] | Array of file IDs from the media import endpoint or an upload link, once it is ready (preferred; mediaIds is accepted too). An ID that does not exist on your account is refused with 404 media_not_found |
post.content.mediaUrls | string[] | Array of public URLs (alternative to mediaID). Whether a file is an image or a video is read from the end of its URL, so use URLs that end in the file extension (.jpg, .png, .mp4 …), or import the file and pass its mediaID |
post.scheduledFor | string | number | When to publish: an ISO 8601 date and time with a time zone (for example 2026-09-30T10:00:00Z or 2026-09-30T12:00:00+02:00), or a Unix time in seconds or milliseconds. More than 60 seconds ahead, the post is scheduled for that time (202 scheduled), up to 365 days ahead and at most 500 posts waiting per account; otherwise it is published at once. A time without a zone (even one in the past), a value that is not a date and time, or a time more than 365 days ahead is refused with 400 scheduled_for_invalid, and error.reason says which (no_time_zone, unreadable, too_far_ahead). See Schedule and cancel |
Media is required, except for text-only posts: these are accepted for Threads, X, LinkedIn, Skool and Facebook. A Facebook post with no media field is a text post on both URLs: facebook.postType can be left out (or set to "text"), and content.text is required. Without a postType, a media field sent empty ([] or null) is refused with 400 on both URLs, so a failed media step never publishes the text alone; with postType "text" it is a text post. Skool needs a paid plan (Beginner or higher) and X needs the X add-on ($9/month, any plan); every other network is available on every plan.
Platform-Specific Options
Put a platform object inside post, next to content (for example post.tiktok). Each platform supports different options. A field we do not use does not stop the post: it is sent, and the answer names the field in warnings (see Fields we did not use, below).
File Size Limits per Network
The media import accepts videos up to 5 GB, but each network has its own publishing limit. Video sizes on every network, and image sizes on TikTok, Instagram, X, Pinterest and Threads, are checked on the server before anything is sent to the network: a file that is too large for the chosen network is refused with 413 and the error code MEDIA_TOO_LARGE (example below). Image sizes for Facebook, LinkedIn and Skool are not checked on our side: those networks decide.
Video: YouTube, Facebook, X, LinkedIn and Skool 5 GB, TikTok 4 GB, Threads 1 GB, Instagram 300 MB (Reels; Stories 100 MB). Pinterest accepts images only (20 MB). How long a big video takes per network: see the File size limits page.
Image limits and durations are listed on the platform file limits page.
Example
curl -X POST 'https://status200uploads.com/api/v2/posts' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer rl_your_api_key' \
-d '{
"post": {
"accountId": "@myprofile",
"platform": "instagram",
"content": {
"text": "Check out this reel! #trending",
"mediaID": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
},
"instagram": {
"postType": "reel",
"shareToFeed": true
}
}
}'Waits and Timeouts
- - Every answer arrives within about 25 seconds. Set the timeout of your HTTP client to 30 seconds or more.
- - A scheduled post (
post.scheduledFor) is answered at once with 202scheduled: nothing is sent until its time, and it takes no 20-second wait. - - A
mediaIDwhose import is still running is waited for up to about 12 seconds. If it is not ready by then, the answer is 409media_processingand nothing is published. - - A network that has not answered after about 24 seconds gets 202
still_publishing: publishing continues in the background and the outcome appears in History within a few minutes, or pollGET /api/v2/posts/{status200.post_id}every 30 seconds, for up to 10 minutes, untildoneis true. - - YouTube uploads always continue in the background (202,
data.status"processing"). So do large videos on X and Skool (over 100 MB), LinkedIn (over 200 MB) and Facebook (over 1 GB): 202 with ajob_idand apost_id, sent in the background, result in History. TikTok and Instagram answer 200 with"processing"and finish the post after the response.
{
"data": {
"status": "processing",
"post_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"container_id": "17912345678901234",
"message": "Post is being processed. Check post history for updates."
},
"status200": {
"post_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"scheduled_post_id": null,
"status_url": "https://status200uploads.com/api/v2/posts/b2c3d4e5-f6a7-8901-bcde-f12345678901"
}
}Our ids: the status200 field
Every answer that made a post in History or a scheduled post, whatever its status (a 200, a 202, and a failure the network reported), carries a top-level status200 field: {"post_id": "<uuid>" | null, "scheduled_post_id": "<uuid>" | null, "status_url": "https://status200uploads.com/api/v2/posts/<id>"}. Both ids are always present (one can be null), and status_url reads the post (or, for a scheduled post, the scheduled post) with GET. Use it rather than data.post_id, which is the network's own id on some networks (Facebook, LinkedIn, Skool) and is left exactly as it was. There is no status200 on a dry run or on a refusal answered before any post was recorded (most 400s, a wrong profile, a 401, an allowance used up). Any answer that left a post in History carries it, whatever its status code, a failed post included. Both URLs send it.
Fields we did not use
A successful answer can carry a warnings array at its top level. Each entry names a field of your request that was not used and says why: a misspelled option, an option block placed next to post instead of inside it, tiktok.privacyStatus instead of privacyLevel, or an option we do not support (X madeWithAi and paidPartnership, LinkedIn carousel). It also says when a TikTok photo was sent as a JPEG copy or as a smaller copy (a resized_for_tiktok entry also has photo, photos, size and copy_size: see TikTok Photos), and names TikTok settings a photo post cannot use (disabledDuet, disabledStitch, isAiGenerated) or a video cannot use (autoAddMusic). For YouTube and TikTok photo posts it says what was changed so that the title and the description fit: title_shortened (the title was over 100 characters on YouTube or 90 on TikTok, or a youtube.title had more than one line), description_shortened (over 5,000 bytes on YouTube or 4,000 characters on TikTok), characters_removed (YouTube does not allow < or >), text_partly_used (the title was made from content.text and the rest of the text was not used, because a description was sent) and title_missing (no text and no youtube.title, so the video is called "Untitled"). Each entry has a code: unknown_field, field_not_used, options_outside_post, converted_to_jpeg, resized_for_tiktok, title_shortened, description_shortened, characters_removed, text_partly_used or title_missing. The post was still sent. Correct the request so the option takes effect next time.
A scheduled post (202 scheduled) can also carry: media_url_not_kept (it uses mediaUrls, or a Reel's instagram.coverUrl, which are fetched when the post goes out, so the file must still be at that address then; a cover that is gone by then leaves the Reel without its cover), skool_one_post_per_hour (another Skool post of your account is scheduled within an hour of it, and Skool takes one post per hour from an account, so one of them will fail), posts_close_together (another post of your account on the same network is scheduled within 20 seconds of it: posts due together go out one after another with no gap, and a network can refuse posts that arrive that close together, so leave at least a minute between them), and caption_over_limit or the title and description codes above, for what the network will refuse or what will be shortened when it goes out.
post.scheduledFor more than 60 seconds ahead): the post is saved and published at that time, usually within a few minutes of it. Nothing was sent yet, and your plan's allowance is checked when it goes out. Keep scheduled_post_id: DELETE /api/v2/posts/{scheduled_post_id} cancels it. Never send this request again without the same Idempotency-Key: it would schedule the post twice (with the key, this answer comes back and nothing is scheduled again).{
"scheduled": true,
"code": "scheduled",
"message": "Scheduled for 2026-10-01T09:00:00.000Z. It will be published then, usually within a few minutes of that time. Your plan's allowance is checked when it goes out, not now. To cancel it, send DELETE https://status200uploads.com/api/v2/posts/c3d4e5f6-a7b8-9012-cdef-123456789012.",
"scheduled_post_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"scheduled_at": "2026-10-01T09:00:00.000Z",
"platform": "tiktok",
"profile_id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"data": {
"status": "scheduled",
"scheduled_post_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"scheduled_at": "2026-10-01T09:00:00.000Z"
},
"status200": {
"post_id": null,
"scheduled_post_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"status_url": "https://status200uploads.com/api/v2/posts/c3d4e5f6-a7b8-9012-cdef-123456789012"
}
}upgrade_url links the plans with a higher daily cap, and status200 names the scheduled post (DELETE /api/v2/posts/{scheduled_post_id} cancels it).{
"queued": true,
"code": "queued_for_next_day",
"message": "Daily post limit reached for this profile (20/20 today, all platforms combined). Post queued for 2026-09-19T14:30:00.000Z.",
"upgrade_url": "https://status200uploads.com/pricing",
"scheduled_post_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"scheduled_at": "2026-09-19T14:30:00.000Z",
"limit": 20,
"used": 20,
"plan": "beginner",
"status200": {
"post_id": null,
"scheduled_post_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status_url": "https://status200uploads.com/api/v2/posts/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}Idempotency-Key, it gets this same answer back and nothing is sent twice). post_id and status200 (the History entry) are included whenever the post is already in History: always for TikTok, Facebook and YouTube. Poll status200.status_url every 30 seconds, for up to 10 minutes, until done is true, to learn the outcome. On X, LinkedIn, Pinterest, Threads, Skool and Instagram a late answer can come before the post is in History and then has no status200: it appears in GET /api/v2/posts?platform=... once the network answers.{
"success": false,
"status": "unknown",
"code": "still_publishing",
"retry": false,
"message": "The network is taking longer than this request can wait, so the outcome is not known yet. Publishing continues in the background and the result appears in the post history within a few minutes. Do not send this request again: it may already have been published.",
"post_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"status200": {
"post_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"scheduled_post_id": null,
"status_url": "https://status200uploads.com/api/v2/posts/b2c3d4e5-f6a7-8901-bcde-f12345678901"
}
}202 is also the answer for every YouTube upload and for large videos on X, LinkedIn, Facebook and Skool: data.status is "processing", with a job_id and a post_id, and the upload continues in the background. Only a body with "queued": true means the post was moved to the next day; "code": "scheduled" means it was scheduled for the time you asked. A request with a time that got no answer in time is answered 202 schedule_unconfirmed: it may have been scheduled, so check the Scheduled page before sending it again.
mediaID does not exist on your account: it was never imported or uploaded, it was deleted, it was removed after 7 days (a file a scheduled post uses is kept until that post goes out), or it is an upload link's file that is not ready yet (complete it first). The answer comes at once. Import the file again from its URL (POST /api/v2/media), or upload it again from the computer through an upload link (paid plans; see Upload from a computer), and use the new file_id.{
"error": {
"code": "media_not_found",
"message": "Media a1b2c3d4-e5f6-7890-abcd-ef1234567890 was not found on the account that owns this API key. It was never uploaded, it was deleted (uploaded files are kept for 7 days, and until a scheduled post that uses them goes out), or it is an upload that has not finished: finish it with POST /api/v2/media/uploads/a1b2c3d4-e5f6-7890-abcd-ef1234567890/complete and send this request again once it answers \"ready\". Otherwise upload it again (from a public URL: POST /api/v2/media; from a computer, on a paid plan: POST /api/v2/media/uploads), then send this request with the new file_id.",
"file_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}mediaID is still being imported after about 12 seconds of waiting. Nothing was published. The response carries a Retry-After: 30 header. Wait that long, or poll the media status until it is "ready", then send the same request again.{
"error": {
"code": "media_processing",
"message": "Media a1b2c3d4-e5f6-7890-abcd-ef1234567890 is still being imported (42%). Poll GET /api/v2/media?file_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890 until status is \"ready\", then send this request again.",
"file_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"progress": 42,
"retry_after_seconds": 30
}
}{
"error": {
"code": "MEDIA_TOO_LARGE",
"message": "This video is 412.5 MB; Instagram accepts videos up to 300 MB. This is Instagram's own limit for Reels posted through its API (15 minutes; Stories 100 MB and 60 seconds).",
"platform": "instagram",
"size_bytes": 432537600,
"limits_url": "https://status200uploads.com/docs/file-limits",
"max_video_bytes": 314572800,
"max_image_bytes": 8388608
}
}Errors have the shape {"error": {"code": "...", "message": "..."}}, and every error from this endpoint carries error.code and error.message. An error reported by a network is passed on with the network's own status, code and message; when the network gives no code, one is derived from the status (bad_request, unauthorized, forbidden, platform_error and so on). Check error.code rather than the status alone. A refusal carries the code, the message and, for a plan refusal, upgrade_url, with the facts of that refusal next to them; what may be sent again, and how, is the x-status200-retry table of the OpenAPI file and the list below.
| Status | Error | Description |
|---|---|---|
| 400 | Bad Request | Invalid JSON, missing post, accountId or platform, unknown platform, no media where media is required, a media field sent as text instead of a list, a Facebook text post without content.text, the network is not connected for this profile (api_error), or a network field that is missing or not allowed (Pinterest boardId or a video pin, Skool group and title, Threads text over 500 characters or more than 20 files), or a post.scheduledFor that cannot be used (scheduled_for_invalid, nothing was sent; error.reason: no_time_zone for a time without a zone, unreadable for a value that is not a date and time, too_far_ahead for more than 365 days ahead), media_not_available (a mediaUrls address of a post with a time answered "not found": upload the file and send its mediaID), video_size_unknown (a TikTok video whose host does not say its size), bad_request for a DELETE whose id is not a UUID, or an accountId that matches two of your profiles once capital letters are ignored (profile_name_ambiguous: error.profiles lists them; send the exact name or the profile id). youtube_tags_too_long: the YouTube tags are over 500 characters the way YouTube counts them (error.tags_length, error.max_tags_length and error.remove_tags say by how much and how many tags to drop); nothing was uploaded. A network can also refuse a post with 400, for example pinterest_sandbox_board. no_publish_id: TikTok did not confirm the upload, so it did not go through: send it again. idempotency_key_invalid: the Idempotency-Key header is empty, longer than 255 characters or not printable ASCII; nothing was sent. pinterest_board_required: a Pinterest post without pinterest.boardId (GET /api/v2/accounts/{id}/options lists your boards). media_wrong_for_post_type: an Instagram or Facebook postType the files cannot make (a Reel or a video post with a picture, a carousel with one file or more than 10, several files on an Instagram post that is not a carousel). options_outside_post: such a refusal, when the option block that would have fixed it was sent next to post instead of inside it (move it inside post). no_container: Instagram did not return a container for the media. The networks' own refusals: pinterest_rejected and x_rejected (with the network's own status, so also 403, 404, 409, 422, or 429 for Pinterest's rate limit), skool_rejected, and category_required (the Skool community needs a category: send skool.label). A LinkedIn refusal, for example of a duplicate post, is a 500 platform_error with LinkedIn's reason in the message |
| 401 | Unauthorized | unauthorized: missing or invalid API key (the word Bearer is read in any capital letters). A network that no longer accepts the profile's sign-in can also answer 401 with its own code: reconnect_required (Facebook, LinkedIn, Skool), RECONNECT_REQUIRED (Instagram), REFRESH_TOKEN_EXPIRED (TikTok), NO_TOKEN (Instagram, TikTok), x_rejected or pinterest_auth_failed: reconnect that network on the Connections page. TOKEN_REFRESH_FAILED (Instagram, TikTok): the sign-in could not be renewed just now; try again later, and reconnect the network if it keeps failing |
| 403 | Forbidden | plan_required: Skool needs the Beginner plan or higher, X needs the X add-on (error.required says which, error.platform names the network, error.plan your plan, and error.upgrade_url links to the page that sells it). account_not_found: the profile is not on the account that owns the API key (error.profiles lists your profiles). x_rejected, pinterest_rejected: X or Pinterest refused the post, for example because the text duplicates an earlier post. forbidden: a network refused it with 403 and no code of its own |
| 404 | Not Found | media_not_found: a mediaID does not exist on your account (or it is an upload link's file that is not ready yet: POST /api/v2/media/uploads/{file_id}/complete first). post_not_found: DELETE named no scheduled post of your account (it never existed, or it is another account's). not_found: there is no endpoint at this path. pinterest_rejected, x_rejected: the network's own 404 (for example a Pinterest boardId that does not exist) |
| 405 | Method Not Allowed | method_not_allowed: /api/v2/posts accepts POST and GET (and GET or DELETE with an id: /api/v2/posts/{id}); the message says so |
| 409 | Conflict | media_processing: a mediaID is still being imported. Wait for Retry-After or poll the media status, then send the post again. scheduled_queue_full: the account already has 500 posts waiting; cancel some or wait for them to go out. not_cancellable: the post is being published, was published, or failed (error.status says which), or it cannot be cancelled with an API key; nothing changed. idempotency_in_progress: the first request with this Idempotency-Key is still being worked on; send it again after the Retry-After seconds (5, also in error.retry_after_seconds) to get its answer. idempotency_outcome_unknown: the first request with this key never finished (more than 10 minutes); check GET /api/v2/posts or History, and send it with a new key if the post is not there |
| 413 | Payload Too Large | MEDIA_TOO_LARGE: the file exceeds the limit of the chosen network. Nothing was sent to the network |
| 422 | Unprocessable | media_failed: the import of a mediaID failed (the message gives the reason). Import the file again. photo_format_not_supported, photo_too_large_to_convert, photo_conversion_failed: a TikTok photo that cannot be sent as JPEG (HEIC, AVIF, BMP or TIFF, over 6,000,000 pixels, or a damaged file). Nothing was posted: save the photo as a JPEG and send it again. photo_too_large_to_resize: a TikTok photo larger than 1080 × 1920 pixels that we cannot make smaller (a WebP, a JPEG over 5 MiB or about 13 MP, a progressive JPEG over 2 MiB or about 12 MP, or a JPEG we cannot read; the full list is in TikTok Photos, under Guides). error.photo says which photo, counting from 1. Nothing was posted: send a JPEG of at most 1080 × 1920 pixels. idempotency_key_reused: this Idempotency-Key was used in the last 24 hours for a different body (error.first_used_at); nothing was sent, use a new key |
| 429 | Too Many Requests | Every 429 carries error.retry_after_seconds: how many seconds to wait before the same request can succeed. The Retry-After header is sent only when the wait is an hour or less; a longer wait (for example a used-up monthly allowance) is only in the body. rate_limited: less than 20 seconds since your previous post to the same network, or Skool's own limit (one post per hour, 10 per day): the Retry-After header, error.retry_after_seconds and the message say how many seconds to wait. daily_attempts_exceeded: too many publish attempts for the profile today, try again after 00:00 UTC (error.attempts, error.ceiling, error.plan). monthly_limit_reached: the Free plan's posts this month are used (error.limit is 30; until 18 October 2026 it is 60 for accounts created since 18 September 2026; posts published from 1 October 2026 count toward October's 30, so from 18 October 2026 error.used can be above error.limit until 1 November 2026; on 17 October 2026 it also answers a request over the day's 5 of an older Free account whose October already has 30, instead of the next-day queue); error.upgrade_url, error.used, error.limit, error.window ("month"), error.resets_at (the 1st of next month), error.plan ("free") and error.trial_available come with it, and nothing is queued. x_limit_reached / x_link_limit_reached: the X add-on allowance is used (error.used, error.limit, error.resets_at, error.plan; for links also error.link_used, error.link_limit and error.detected_links). Using up a daily allowance (Beginner, Apprentice) is answered with 202, not 429 (only requests that reach the limit at the same moment can get 429 daily_limit_reached). A post with a time (post.scheduledFor) takes no 20-second wait and is never refused for the allowance: that is checked when it goes out |
| 500 | Server Error | server_error: publishing failed on our side (read error.message; with the same Idempotency-Key an answer whose outcome is not known is answered again). platform_error: the network failed without a code of its own (Threads also reports a sign-in it no longer accepts this way, and LinkedIn a refusal such as a duplicate post: read error.message). api_error: the post could not be queued for the next day (the database's own message: send it again with the same Idempotency-Key, or check the Scheduled page). server_error on a post with a time or on a DELETE: see the message (nothing was scheduled or cancelled, or check the Scheduled page) |
| 502 | Bad Gateway | upstream_unavailable: the request could not be sent, nothing was posted. upload_worker_failed: our upload of a TikTok video stopped before TikTok confirmed it, so it did not go through. Send the request again. (A TikTok photo post whose upload stops after TikTok accepted it is answered 202 still_publishing instead: do not send it again.) |
| 503 | Service Unavailable | skool_unavailable: Skool publishing is temporarily unavailable. x_unavailable: X is not accepting posts from Status 200 Uploads right now. Try again later. caller_check_unavailable: we could not check who sent the request in time, and nothing was posted. Send the same request again after the Retry-After seconds (5). schedule_check_unavailable: the posts waiting on the account could not be counted, so nothing was scheduled; send it again after the Retry-After seconds (5). idempotency_unavailable: the Idempotency-Key could not be checked just now, so nothing was sent; send the same request with the same key after the Retry-After seconds (5) |
| 504 | Gateway Timeout | cancel_unconfirmed: a DELETE whose answer did not arrive in time. Send the same DELETE again after the Retry-After seconds (5): cancelling twice is safe |
When to Send a Request Again
- - With an
Idempotency-Key, a timeout or a lost connection is safe to retry: send the same request with the same key, and a post that did go out is answered again (Idempotent-Replayed: true) instead of being posted twice. 409idempotency_in_progress: wait for theRetry-Afterseconds (5) and send it again. See Safe Retries. Without a key, the rules below apply. - - Never after a 200, or a 202 with
still_publishingor"processing", unless you send the same Idempotency-Key: the post is published or on its way. Check History instead, or pollGET /api/v2/posts/{status200.post_id}every 30 seconds, for up to 10 minutes, untildoneis true. A 202 with"queued": truepublishes by itself on the next day. - - Never after a 202
scheduledwithout the same Idempotency-Key: sending it again schedules the post a second time (branch oncode). After a 202schedule_unconfirmed, check the Scheduled page first. ADELETEcan always be sent again: cancelling twice is safe (the second answer is 200 withalready_cancelled: true), and 504cancel_unconfirmedasks you to. - - 409
media_processing: wait for theRetry-Afterseconds (30), then send the same request. - - 429 throttle: wait for the
Retry-Afterseconds (the same number is inerror.retry_after_seconds, for tools that read only the body, such as n8n), then send the same request.daily_attempts_exceeded: stop until 00:00 UTC.monthly_limit_reached,x_limit_reached,x_link_limit_reached: do not resend beforeresets_at(for a link post you can remove the link). - - 502
upstream_unavailableand 503skool_unavailable/x_unavailable: send it again later. - - 503
caller_check_unavailableandidempotency_unavailable: nothing was posted. Wait for theRetry-Afterseconds (5, also inerror.retry_after_seconds), then send the same request. - - Other 4xx answers will not succeed unchanged: fix what the message names first (a refusal is not remembered, so the fixed request can use the same Idempotency-Key; 422
idempotency_key_reusedasks for a new key). After a 500, read the message and check History before sending again (with the same Idempotency-Key, a 500platform_errorafter the post was handed to the network is answered again as it was: if the post is not in History, send it with a new key).
Checking a Post Without Sending It
Send "dryRun": true next to post (or inside it) to check a post without sending it. You can also send the post as validate instead of post. Every check a publish makes is run and nothing is sent. The answer is 200 with dry_run: true, an outcome (publish, queue, schedule for a post with a time, or refuse), would_publish, a reason when it would not be sent now, and warnings. The reason carries the code and status this URL would answer, for example 403 account_not_found. A check is not counted in your API key's request count. "dryRun": false publishes as usual. Any other value is refused with 400 bad_request and nothing is published. A check that gets no report in time is answered 503 dry_run_unavailable with Retry-After: send it again (nothing was sent).
Differences on the older URL
Both URLs publish through the same engine, and https://app.status200uploads.com/functions/v1/api-posts takes the same request. What is still different there: it waits up to 90 seconds for a mediaID that is still importing, so a request can stay open for about two minutes (its 409 names GET /api-media-upload?file_id=); an unknown profile is answered 404 api_error instead of 403 account_not_found (error.profiles lists your profiles, and the refusal does not start the 20-second wait); an error from a network is passed on without an added error.code; and "No media provided" and a media field sent as text have the code api_error and no Facebook hint (this URL answers TikTok, Facebook and YouTube posts with bad_request and the hint). Its own refusals of the request use api_error with other words: 401 for a missing or unknown key, 400 for invalid JSON, a missing post, accountId or platform and a dryRun that is not true or false, and 405 "Method not allowed" for a GET or any other method. The order of its checks differs: the key is read before the JSON, scheduledFor before the YouTube tags, and the plan gate (403) and the 20-second wait (429) come before the profile. Its media_not_found has other words. It never answers 502 upstream_unavailable, 503 dry_run_unavailable, 202 schedule_unconfirmed or 504 cancel_unconfirmed, which are this URL's own. A POST publishes on any path under /api-posts: send it to /api-posts itself. Its dry-run reason uses these codes too. Its 429 has the same Retry-After header and error.retry_after_seconds. It schedules a post with post.scheduledFor the same way, and DELETE https://app.status200uploads.com/functions/v1/api-posts/{id} cancels one with the same answers as DELETE /api/v2/posts/{id}. Its answers carry the same status200 field. It takes the same Idempotency-Key and shares it with this URL: a key sent to one and then the other gets the first answer again (send retries to the same URL, all the same). The reads (GET /api/v2/accounts, /accounts/{id}/options, /posts and /posts/{id}) are on https://status200uploads.com/api/v2 only, with the same key: the older URL answers a GET with 405.
/api/v2/postsSchedule a post for later with post.scheduledFor, and cancel it with DELETE before it goes out.
Send the same request as for publishing, with post.scheduledFor set to the time the post should go out. Everything a post is checked for now is checked at once (the profile, the connection, the media, the network's options, and the plan: Skool needs Beginner or higher and X the X add-on, 403 plan_required). The post is then saved, and the answer is 202 with "code": "scheduled" and a scheduled_post_id. It works on every plan, Free included, and a scheduled post takes no 20-second wait, so a calendar of posts can be sent in a row.
The time
- - An ISO 8601 date and time with a time zone:
2026-09-30T10:00:00Z,2026-09-30T12:00:00+02:00,2026-09-30T10:00:00.000Z. A Unix time in seconds or milliseconds (a number) works too, and so does a date with the month as a word and a zone, as e-mail, HTTP and JavaScript'sDate.toString()write it (Wed, 30 Sep 2026 10:00:00 GMT,... +0200,... EDT). - - A date written with numbers only (
01/10/2026 10:00 GMT) isunreadable, even with a zone: it is 10 January in one country and 1 October in another. - - A time without a zone (
2026-09-30T10:00:00, or a date alone) is refused with 400scheduled_for_invalid,error.reasonno_time_zone, even when it is in the past: we cannot tell which moment you mean. The message shows your time with a zone added. - - More than 60 seconds ahead is scheduled; the past,
"now"or within 60 seconds publishes at once, as without the field. - - Up to 365 days ahead (
too_far_aheadafter that), and at most 500 posts waiting per account (409scheduled_queue_full). A value that is not a date and time isunreadable.
curl -X POST 'https://status200uploads.com/api/v2/posts' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer rl_your_api_key' \
-d '{
"post": {
"accountId": "@myprofile",
"platform": "tiktok",
"scheduledFor": "2026-10-01T09:00:00Z",
"content": { "text": "Coming Thursday", "mediaID": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"] },
"tiktok": { "privacyLevel": "PUBLIC_TO_EVERYONE" }
}
}'
# Cancel it before it goes out
curl -X DELETE 'https://status200uploads.com/api/v2/posts/c3d4e5f6-a7b8-9012-cdef-123456789012' \
-H 'Authorization: Bearer rl_your_api_key'Cancelling
DELETE https://status200uploads.com/api/v2/posts/{scheduled_post_id} cancels a post that has not gone out, whether the API, the dashboard or a connected AI app scheduled it, or the daily allowance queued it (the 202 of a queued post has a scheduled_post_id too). The same key, no body. The older URL takes DELETE https://app.status200uploads.com/functions/v1/api-posts/{id}. A DELETE is counted in the key's request count like any request.
- - 200:
{"data": {"scheduled_post_id", "status": "cancelled", "platforms", "was_due_at", "message"}}. Sent again, the answer is 200 with"already_cancelled": true: cancelling twice is safe. - - 404
post_not_found: no scheduled post with that id on your account. 400bad_request: the id is not a UUID. - - 409
not_cancellable, witherror.status: it is being published right now (processing), it was published, or it failed when it was due. A post that is on its way cannot be stopped, and Status 200 Uploads cannot remove a post from a network. If a post that was being published is moved to a later time instead (a used-up daily allowance moves it to the next day), send the same DELETE again. - - 504
cancel_unconfirmed(with Retry-After): send the same DELETE again.
What is checked when it goes out
- - Your plan's allowance, when the post goes out, not when it is scheduled. Over a daily allowance (Beginner, Apprentice) the post moves to the same time on the next day. On Free, a post over the 30 posts of the month fails: this shows on the Scheduled page and in the failure email, not in History.
- - X: the X add-on's allowance at that time. Skool: one post per hour per account, across all its profiles (a warning
skool_one_post_per_hoursays when two scheduled Skool posts of your account are closer), and its daily cap. - - The network itself, with the options you sent: they are kept with the post and sent as a post published at once would send them.
Media
A file imported with POST /api/v2/media (or uploaded through an upload link) and sent as mediaID is kept until the post goes out, however far ahead it is. mediaUrls are fetched when the post goes out, not now, and no copy is kept: the file must still be at that address then (the answer warns with media_url_not_kept, and an address that already answers "not found" is refused with 400 media_not_available). For anything more than a few hours ahead, import the file and send its mediaID. A Reel's instagram.coverUrl is fetched when the Reel goes out too: the same warning, the same 400 for a cover that is already gone, and a cover that is gone by then leaves the Reel without its cover.
Timing and where to see it
- - A scheduled post is published at its time, usually within a few minutes of it. Posts due at the same moment go out one after another with no gap between them, so a large batch takes longer, and a network can refuse posts that arrive that close together: leave at least a minute between posts on one network (the warning
posts_close_togethersays when two are within 20 seconds). - - It is listed on the Scheduled page and the calendar of the dashboard, which can also cancel it, and in History once it has gone out. Through the API,
GET /api/v2/posts/{scheduled_post_id}(the 202'sstatus200.status_url) reads it before it goes out and, afterwards, each network's result;GET /api/v2/posts?status=scheduledlists the posts still waiting. - - Revoking an API key does not cancel the posts it scheduled.
- - There is no endpoint to change a scheduled post yet: cancel it and schedule it again.
- - A request with a time sent twice schedules the post twice: never send it again after a 202
scheduledunless it carries the sameIdempotency-Key(then the first answer comes back and nothing is scheduled again), and check the Scheduled page after a 202schedule_unconfirmed. - - A dry run with a time answers the outcome
scheduleand schedules nothing.
/api/v2/accountsYour profiles, the networks connected to each one, and whether each connection can post right now.
Full URL: GET https://status200uploads.com/api/v2/accounts. Use a profile_id (or the handle) as post.accountId when you post, instead of guessing a name. No parameters.
- - The same API key as for posting, in
Authorization: Bearer rl_your_api_key. You only ever see your own account: another account's id reads exactly like an id that does not exist. - - On
https://status200uploads.com/api/v2only. The older URL answers a GET with 405. - - 60 reads a minute per account, all your API keys together (posting options also 10 a minute, see Rate Limits). Over the limit: 429
rate_limitedwith aRetry-Afterheader anderror.retry_after_seconds. Reads are not counted in your API key's request count, so a key used only for reads shows 0 requests on the API page: it is still in use. - - Answers carry
Cache-Control: private, no-store: do not cache them between API keys.
curl 'https://status200uploads.com/api/v2/accounts' \
-H 'Authorization: Bearer rl_your_api_key'health is "ready" (it can post, including an expired sign-in the network renews by itself on the next post: X, YouTube, LinkedIn, Pinterest, TikTok), "reconnect_required" (reconnect it on the Connections page) or "unknown". connected_at is when it was connected, updated_at when its sign-in last changed.{
"data": [
{
"profile_id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"profile_name": "myshop",
"handle": "@myshop",
"created_at": "2026-03-01T10:00:00+00:00",
"networks": [
{
"platform": "pinterest",
"username": "myshop",
"status": "connected",
"health": "ready",
"access_expires_at": "2026-11-20T08:12:00+00:00",
"connected_at": "2026-03-02T09:30:00+00:00",
"updated_at": "2026-09-20T08:12:00+00:00"
}
]
}
]
}| Status | Error | Description |
|---|---|---|
| 401 | Unauthorized | unauthorized: missing or invalid API key (the same answers as for a POST) |
| 429 | Too Many Requests | rate_limited: this account sent 60 reads in this minute, all its API keys together (error.limit 60), or asked for posting options 10 times (error.limit 10). Wait for the Retry-After seconds (also in error.retry_after_seconds), then send it again |
| 500 | Server Error | server_error: it could not be read just now, and nothing changed. Try again in a minute |
| 405 | Method Not Allowed | method_not_allowed: /api/v2/accounts answers GET only (Allow: GET, OPTIONS) |
/api/v2/accounts/{id}/optionsWhat one profile may actually do on each network, asked of the networks themselves: TikTok privacy levels, Pinterest boards, Skool groups and labels, the Facebook Page, the Instagram publishing limit, YouTube's choices.
Full URL: GET https://status200uploads.com/api/v2/accounts/{profile_id}/options. Ask before you post, so the post is built from what the account allows: the only TikTok privacy levels it will accept in post.tiktok.privacyLevel, the board ids for post.pinterest.boardId, the group ids for post.skool.group. Each network that is asked is a real call with your own sign-in, so answers are reused for about a minute and a half, and the answer changes slowly: reuse it.
- - The same API key as for posting, in
Authorization: Bearer rl_your_api_key. You only ever see your own account: another account's id reads exactly like an id that does not exist. - - On
https://status200uploads.com/api/v2only. The older URL answers a GET with 405. - - 60 reads a minute per account, all your API keys together (posting options also 10 a minute, see Rate Limits). Over the limit: 429
rate_limitedwith aRetry-Afterheader anderror.retry_after_seconds. Reads are not counted in your API key's request count, so a key used only for reads shows 0 requests on the API page: it is still in use. - - Answers carry
Cache-Control: private, no-store: do not cache them between API keys.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
platforms | string | Only these networks, comma-separated (tiktok, instagram, facebook, youtube, x, linkedin, pinterest, threads, skool). Named networks are always answered, connected or not. Default: every network connected to the profile |
skool_group_slug | string | Which Skool group to read the labels of (1 to 200 characters). With exactly one group, its labels come back anyway |
curl 'https://status200uploads.com/api/v2/accounts/d4e5f6a7-b8c9-0123-def0-234567890123/options?platforms=tiktok,pinterest' \
-H 'Authorization: Bearer rl_your_api_key'status is "ok" (with options), "not_connected", "reconnect_required", "unavailable" (the network could not be asked just now; the rest of the answer is still good) or "no_options" (X, LinkedIn and Threads have nothing to look up). Every other status has a note saying why. The answer is 200 even when a network fails: that is a row, never an error. cached is true when the row is reused, and checked_at says when the network was really asked.{
"data": {
"profile_id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"profile_name": "myshop",
"networks": [
{
"platform": "tiktok",
"status": "ok",
"options": {
"privacy_level_options": ["PUBLIC_TO_EVERYONE", "SELF_ONLY"],
"comment_disabled": false, "duet_disabled": true, "stitch_disabled": false,
"max_video_post_duration_sec": 600,
"creator_username": "myshop", "creator_nickname": "My Shop"
},
"note": "privacy_level_options are the only values TikTok will accept in post.tiktok.privacyLevel for this account. ...",
"checked_at": "2026-09-27T12:00:00.000Z", "cached": false, "error_code": null
},
{
"platform": "pinterest",
"status": "ok",
"options": { "boards": [{ "id": "900123", "name": "Recipes", "privacy": "PUBLIC", "pin_count": 12 }] },
"note": "Send one of these ids as post.pinterest.boardId when you post to Pinterest.",
"checked_at": "2026-09-27T12:00:00.000Z", "cached": false, "error_code": null
}
],
"note": null,
"asked_at": "2026-09-27T12:00:00.000Z"
}
}By network: tiktok privacy_level_options, comment/duet/stitch settings and max_video_post_duration_sec; pinterest boards (id, name, privacy, pin_count); skool groups (id, slug, name) and the labels of one group; facebook the Page this profile posts to; instagram how much of its rolling publishing limit is used (a null is not a zero); youtube the categories, the privacy choices (post.youtube.privacyStatus is required on every YouTube post) and the tag budget. Nothing a network says is passed on except a short error_code.
| Status | Error | Description |
|---|---|---|
| 400 | Bad Request | bad_request with error.parameter: the id is not a profile id (id), platforms names a network that does not exist (platforms), or skool_group_slug is empty or over 200 characters |
| 404 | Not Found | account_not_found: no profile with that id on your account. No network was asked |
| 401 | Unauthorized | unauthorized: missing or invalid API key (the same answers as for a POST) |
| 429 | Too Many Requests | rate_limited: this account sent 60 reads in this minute, all its API keys together (error.limit 60), or asked for posting options 10 times (error.limit 10). Wait for the Retry-After seconds (also in error.retry_after_seconds), then send it again |
| 500 | Server Error | server_error: it could not be read just now, and nothing changed. Try again in a minute |
/api/v2/postsYour posts and your posts waiting to go out, newest first, page by page.
Full URL: GET https://status200uploads.com/api/v2/posts. Every item is either a post (kind "post": one network, its status, its link) or a scheduled post that has not produced a post yet (kind "scheduled_post": waiting, cancelled, or failed before anything was sent). Once a scheduled post has gone out, each network's post is listed instead, with its scheduled_post_id, so nothing is listed twice. Items are ordered by at: when a post was sent, when a scheduled post is due (so posts due in the future come first).
- - The same API key as for posting, in
Authorization: Bearer rl_your_api_key. You only ever see your own account: another account's id reads exactly like an id that does not exist. - - On
https://status200uploads.com/api/v2only. The older URL answers a GET with 405. - - 60 reads a minute per account, all your API keys together (posting options also 10 a minute, see Rate Limits). Over the limit: 429
rate_limitedwith aRetry-Afterheader anderror.retry_after_seconds. Reads are not counted in your API key's request count, so a key used only for reads shows 0 requests on the API page: it is still in use. - - Answers carry
Cache-Control: private, no-store: do not cache them between API keys.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
limit | integer | Items per page, 1 to 100. Default 20 |
cursor | string | next_cursor from the previous page, exactly as it came. Leave it out to start at the newest. A cursor that is not ours is refused with 400 (never read as "start again") |
status | string | One or more of success, failed, processing, timeout, scheduled, cancelled (comma-separated). success, processing and timeout are posts; scheduled and cancelled are scheduled posts; failed is both (failed posts, and scheduled posts that failed before anything was sent). A scheduled post that went out is listed as its posts: use success |
platform | string | One network: tiktok, instagram, facebook, youtube, x, linkedin, pinterest, threads, skool. A scheduled post is listed when it includes that network |
kind | string | post or scheduled_post |
profile_id | string | Only this profile (a UUID from GET /api/v2/accounts) |
curl 'https://status200uploads.com/api/v2/posts?limit=50&status=failed' \
-H 'Authorization: Bearer rl_your_api_key'next_url is the next page with the same filters (null on the last page), for n8n's and Make's pagination. done is true when the item will not change by itself: a post that succeeded or failed; a post that timed out, once nothing is checking it any more (10 minutes after it was sent on Pinterest and Threads, 2 hours on YouTube, 25 hours on X, LinkedIn and Skool, where a large video may still finish, and 2 days on Facebook, Instagram and TikTok, which we ask whether the post is there); a scheduled post that was cancelled or failed before anything was sent. timeout means sent but never confirmed: do not send it again, and once it is done, look for it on the network. error_message is at most 1,000 characters. A post's notices are what we changed to get a TikTok photo post out (for example resized_for_tiktok, a photo sent as a smaller copy: see TikTok Photos), as they were said when TikTok took the post: code, message and, for a photo, photo (for a smaller copy also photos, size and copy_size). A post that went out through a schedule says it here. They are [] when there is none and on the other networks: what we changed there (a YouTube title or description we shortened, for example) is said only in the warnings of the answer that sent or scheduled the post. A waiting scheduled post's note says why it moved (for example a used-up daily allowance). An unknown parameter is named in warnings (code unknown_parameter) and not used.{
"data": [
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"kind": "scheduled_post",
"profile_id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"platforms": ["tiktok", "x"],
"status": "scheduled",
"outcome": "waiting",
"done": false,
"media_type": "video",
"at": "2026-10-01T09:00:00+00:00",
"scheduled_for": "2026-10-01T09:00:00+00:00",
"created_at": "2026-09-27T12:00:00+00:00",
"updated_at": "2026-09-27T12:00:00+00:00",
"note": null,
"error_message": null
},
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"kind": "post",
"profile_id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"platform": "pinterest",
"status": "success",
"done": true,
"media_type": "image",
"publish_id": "1146236468384210033",
"permalink": "https://www.pinterest.com/pin/1146236468384210033/",
"error_message": null,
"at": "2026-09-27T11:58:02.183412+00:00",
"updated_at": "2026-09-27T11:58:04.912+00:00",
"scheduled_post_id": null,
"notices": []
}
],
"next_cursor": "eyJ0IjoiMjAyNi0wOS0yN1QxMTo1ODowMi4xODM0MTJaIiwiaWQiOiJiMmMzZDRlNS1mNmE3LTg5MDEtYmNkZS1mMTIzNDU2Nzg5MDEifQ",
"next_url": "https://status200uploads.com/api/v2/posts?limit=2&cursor=eyJ0IjoiMjAyNi0wOS0yN1QxMTo1ODowMi4xODM0MTJaIiwiaWQiOiJiMmMzZDRlNS1mNmE3LTg5MDEtYmNkZS1mMTIzNDU2Nzg5MDEifQ"
}| Status | Error | Description |
|---|---|---|
| 400 | Bad Request | bad_request with error.parameter (limit, cursor, status, platform, kind or profile_id) and a message saying what it takes. status=published is refused with a hint: a scheduled post that has gone out is listed as its posts (use success) |
| 401 | Unauthorized | unauthorized: missing or invalid API key (the same answers as for a POST) |
| 429 | Too Many Requests | rate_limited: this account sent 60 reads in this minute, all its API keys together (error.limit 60), or asked for posting options 10 times (error.limit 10). Wait for the Retry-After seconds (also in error.retry_after_seconds), then send it again |
| 500 | Server Error | server_error: it could not be read just now, and nothing changed. Try again in a minute |
/api/v2/posts/{id}One post: its status, its link on the network, the error when it failed, and its figures when we have them. Or one scheduled post, before and after it goes out.
Full URL: GET https://status200uploads.com/api/v2/posts/{id}. The id is a post id or a scheduled post id: the status200.post_id or status200.scheduled_post_id of a POST answer (its status200.status_url is this URL, ready to call), or an id from GET /api/v2/posts. To wait until a post is published, call it every 30 seconds until done is true, for up to 10 minutes: most posts are done within a minute or two. Stop after that either way. A post still processing (a large video) finishes by itself and shows in History; a timeout was sent and never confirmed, and can stay so for up to 2 days while we check: look on the network, and never send it again.
- - The same API key as for posting, in
Authorization: Bearer rl_your_api_key. You only ever see your own account: another account's id reads exactly like an id that does not exist. - - On
https://status200uploads.com/api/v2only. The older URL answers a GET with 405. - - 60 reads a minute per account, all your API keys together (posting options also 10 a minute, see Rate Limits). Over the limit: 429
rate_limitedwith aRetry-Afterheader anderror.retry_after_seconds. Reads are not counted in your API key's request count, so a key used only for reads shows 0 requests on the API page: it is still in use. - - Answers carry
Cache-Control: private, no-store: do not cache them between API keys.
curl 'https://status200uploads.com/api/v2/posts/b2c3d4e5-f6a7-8901-bcde-f12345678901' \
-H 'Authorization: Bearer rl_your_api_key'notices included) plus figures (null unless it succeeded). A figure the network did not report is null and named in not_reported: null never means zero. Figures are collected every 6 hours during the first 7 days after publishing, on the networks that report them; polled_at says when, and note says why there are none, or which figures the network usually reports but did not report for that post.{
"data": {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"kind": "post",
"profile_id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"platform": "tiktok",
"status": "success",
"done": true,
"media_type": "video",
"publish_id": "v_pub_7123456789",
"permalink": "https://www.tiktok.com/@myshop/video/7123456789",
"error_message": null,
"at": "2026-09-27T11:58:02+00:00",
"updated_at": "2026-09-27T11:59:40+00:00",
"scheduled_post_id": null,
"notices": [],
"figures": {
"metrics": { "views": 1200, "likes": 34, "comments": 5, "shares": 2, "impressions": null, "reach": null, "saves": null, "clicks": null, "watch_time_seconds": null },
"not_reported": ["impressions", "reach", "saves", "clicks", "watch_time_seconds"],
"engagement": 41,
"polled_at": "2026-09-27T18:00:03+00:00",
"note": null
}
}
}status is scheduled, processing, published, failed or cancelled, and outcome is what became of it: waiting, sending, success, failed, partly_failed (some networks failed), unconfirmed (sent, not confirmed yet) or cancelled. results has one entry per network once it has gone out: that network's post (post_id, status, link), pending while it is still being sent, or not_sent with the reason. done is true for success, failed, partly_failed and cancelled, and for unconfirmed once every network's result is done (each result carries its own done). Each result also carries that network's post's notices (see GET /api/v2/posts above): what we changed to get a TikTok photo post out, which nobody saw when the scheduled post went out (here a photo sent as a smaller copy).{
"data": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"kind": "scheduled_post",
"profile_id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"platforms": ["pinterest", "tiktok"],
"status": "published",
"outcome": "success",
"done": true,
"media_type": "image",
"at": "2026-10-01T09:00:00+00:00",
"scheduled_for": "2026-10-01T09:00:00+00:00",
"created_at": "2026-09-27T12:00:00+00:00",
"updated_at": "2026-10-01T09:01:12+00:00",
"note": null,
"error_message": null,
"results": [
{
"platform": "pinterest",
"post_id": "e5f6a7b8-c9d0-1234-ef01-345678901234",
"status": "success",
"done": true,
"publish_id": "1146236468384210034",
"permalink": "https://www.pinterest.com/pin/1146236468384210034/",
"error_message": null,
"notices": []
},
{
"platform": "tiktok",
"post_id": "f6a7b8c9-d0e1-2345-f012-456789012345",
"status": "success",
"done": true,
"publish_id": "7123456790",
"permalink": "https://www.tiktok.com/@/video/7123456790",
"error_message": null,
"notices": [
{
"code": "resized_for_tiktok",
"message": "Photo too large for TikTok: it is 2160 × 2700 pixels and TikTok takes up to 1080 × 1920, so we sent a smaller copy (1080 × 1350). Your original file is unchanged.",
"photo": 1,
"photos": 1,
"size": { "width": 2160, "height": 2700 },
"copy_size": { "width": 1080, "height": 1350 }
}
]
}
]
}
}| Status | Error | Description |
|---|---|---|
| 400 | Bad Request | bad_request, error.parameter id: the id is not a UUID |
| 404 | Not Found | post_not_found: no post or scheduled post with that id on your account (another account's id reads the same) |
| 401 | Unauthorized | unauthorized: missing or invalid API key (the same answers as for a POST) |
| 429 | Too Many Requests | rate_limited: this account sent 60 reads in this minute, all its API keys together (error.limit 60), or asked for posting options 10 times (error.limit 10). Wait for the Retry-After seconds (also in error.retry_after_seconds), then send it again |
| 500 | Server Error | server_error: it could not be read just now, and nothing changed. Try again in a minute |
/api/v2/mediaImport a media file from a public URL into your media library. The answer contains a file_id to pass in content.mediaID when you create a post.
Import: POST https://status200uploads.com/api/v2/media. Status of an import: GET https://status200uploads.com/api/v2/media?file_id=<file_id>. The older URL https://app.status200uploads.com/functions/v1/api-media-upload accepts the same two requests and keeps working. A file on a computer rather than at a URL (paid plans): see Upload from a computer, below. Never put a file on a public site only to import it from there.
Request Headers
| Parameter | Type | Description |
|---|---|---|
Content-Typerequired | string | application/json (POST only) |
Authorizationrequired | string | Bearer rl_your_api_key |
Idempotency-Key | string | Optional, POST only. 1 to 255 printable ASCII characters naming this one import. The same key with the same body (the same url) within 24 hours gets the first answer again (the same file_id, Idempotent-Replayed: true), never a 429 and never a second file. The two media URLs share keys; posts keep theirs apart. See Safe Retries |
Request Body
| Parameter | Type | Description |
|---|---|---|
urlrequired | string | Public http or https URL of the image or video file itself: JPEG, PNG or WebP, MP4 (M4V too), MOV or WebM. We ask the server hosting it for the file twice before importing (a HEAD, then a ranged GET of one byte, or of the first 4 KB when the server sends no type or a generic one such as application/octet-stream, or when the HEAD fails), and each must be answered within 8 seconds, so this check takes up to about 16 seconds. A file without a usable type is judged by those first bytes. A web page, a GIF, HEIC or SVG image, another video format, or any other file is refused with 415. Google Drive, Dropbox and OneDrive share links: see Share links (rolling out), below |
Images
JPG, PNG, WebP - Max 20 MB
Videos
MP4 (M4V too), MOV, WebM - Max 5 GB
A larger file is refused with 413. These are the import limits: the networks accept less, see platform file limits. Imported files are kept for 7 days, and a file used by a scheduled post is kept until that post goes out, however far ahead it is. A post scheduled with mediaUrls instead fetches them when it goes out, so for anything more than a few hours ahead import the file and send its file_id. One file_id can be used in any number of posts during that time.
curl -X POST 'https://status200uploads.com/api/v2/media' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer rl_your_api_key' \
-d '{"url": "https://example.com/video.mp4"}'{
"success": true,
"file_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"size": 2048576,
"type": "image/jpeg",
"status": "ready"
}{
"success": true,
"file_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "processing",
"message": "Upload started. Large files continue in the background: poll GET /api/v2/media?file_id=... until status is \"ready\" (the response includes progress), then use the file_id in create post.",
"size": 3221225472,
"type": "video/mp4"
}Checking an Import
After a 202, ask for the status until it is "ready". Every 10 to 30 seconds is enough. Status requests are not throttled. The same request follows a file sent through an upload link (see Upload from a computer). If you create a post while the import is still running, the post endpoint answers 409 media_processing and nothing is published. A file_id that is not a UUID is answered 400, and one that is not on your account 404.
curl 'https://status200uploads.com/api/v2/media?file_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890' \
-H 'Authorization: Bearer rl_your_api_key'status is "processing", "ready" or "failed"; success is true only when it is ready. bytes_uploaded and progress (0 to 100) are present when the size of the file is known. error holds the reason when the status is failed.{
"success": false,
"file_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "processing",
"size": 3221225472,
"type": "video/mp4",
"public_url": "https://app.status200uploads.com/storage/v1/object/public/media-files/<user-id>/a1b2c3d4-e5f6-7890-abcd-ef1234567890.mp4",
"error": null,
"bytes_uploaded": 1358954496,
"progress": 42
}Large files and HTTP Range
A large import is downloaded in pieces and continues automatically after an interruption when the source server supports HTTP Range requests (S3, Cloudflare R2, Supabase Storage and most CDNs do). A source without Range support still works, but a very large file cannot be continued after a dropped connection: the import then ends with status "failed" and has to be started again.
Errors from this endpoint have the shape {"error": "message"}. A 429 also carries "code": "rate_limited" and retry_after_seconds next to error, and a Retry-After header, on both URLs; so do the Idempotency-Key answers ("code": "idempotency_in_progress" and so on). A refused share link also carries code, next_step and provider (see Share links, above). Every answer to an import request arrives within about 25 seconds.
| Status | Error | Description |
|---|---|---|
| 400 | Bad Request | Invalid JSON, url missing or not an http(s) URL, or the file could not be downloaded ("Failed to download file: 404 Not Found", or the source did not answer one of the two checks within 8 seconds). On a status request: file_id missing or not a UUID. idempotency_key_invalid: the Idempotency-Key header is empty, longer than 255 characters or not printable ASCII. Where share links are on: link_not_public, link_is_folder, link_not_found, link_temporarily_blocked, link_not_a_file, link_not_supported or share_links_paused, with next_step and provider (see Share links) |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | plan_limit_reached (where share links are on): a share link that needs converting, sent with the key of a Free account. Share links are included in the paid plans, from Beginner; a link that already downloads the file is imported as before. With upgrade_url, next_step and provider |
| 404 | Not Found | Status request: no media with this file_id on your account |
| 405 | Method Not Allowed | A method other than POST (import) and GET (status): {"error": "Method Not Allowed. Use POST /api/v2/media."}, or {"error": "Method Not Allowed"} on the older URL |
| 409 | Conflict | idempotency_in_progress: the first import with this Idempotency-Key is still running; send it again after the Retry-After seconds (5). idempotency_outcome_unknown: the first import with this key never answered; send it again with a new key (a second import does no harm) |
| 413 | Payload Too Large | Image larger than 20 MB or video larger than 5 GB (the declared size is in size). Or the storage refused the file after the import had started: then the answer also has file_id and "status": "failed" |
| 415 | Unsupported Media Type | The URL does not point to a file of a type the media library takes (JPEG, PNG, WebP, MP4, M4V, MOV, WebM): a web page that shows the file, a GIF, HEIC or SVG image, another video format, or a source that sends no type (or a generic one) and whose first bytes are none of these. Convert the file first, or use a direct link to the file itself. Nothing is imported. A shared file of another type names the file in file_name when the provider sends its name; where share links are on, the message of a plain URL that is a page adds that a share link works too |
| 422 | Unprocessable | idempotency_key_reused: this Idempotency-Key was used in the last 24 hours for another body, such as another url (first_used_at); use a new key |
| 429 | Too Many Requests | rate_limited: less than 20 seconds since your previous import. The Retry-After header, retry_after_seconds and the message say how many seconds to wait. import_budget_exhausted (only for the key of a connected AI app): its daily import budget is used; here error is an object with code, message, used_bytes, limit_bytes, remaining_bytes, requested_bytes, resets_at, plan and upgrade_url |
| 500 | Server Error | The import could not be started, or it failed while the request was still open |
| 503 | Service Unavailable | idempotency_unavailable: the Idempotency-Key could not be checked just now and nothing was imported; send the same request with the same key after the Retry-After seconds (5). uploads_busy (where share links are on): the plan behind a share link could not be read just now, and nothing was imported; send it again after the Retry-After seconds (60) |
/api/v2/media/uploadsUpload an image or a video from a computer through a one-time upload link. The file is checked, then it is in your media library with a file_id to post, like an imported file.
Full URLs: POST https://status200uploads.com/api/v2/media/uploads makes a link, POST https://status200uploads.com/api/v2/media/uploads/{file_id}/token gives it a fresh token, POST https://status200uploads.com/api/v2/media/uploads/{file_id}/complete checks the file and puts it in your media library, and DELETE https://status200uploads.com/api/v2/media/uploads/{file_id} cancels an unfinished upload. GET https://status200uploads.com/api/v2/media?file_id=<file_id> says how far it is. On this URL only, with the same API key.
For a file that is on a computer, not at a URL: your own script, or an AI app that runs commands on your computer (tested with Claude Code; other AI apps that can run commands may work). The file goes from that computer straight to storage, never through a public site, and it is in a private place, readable by no one, until it has been checked. A connected AI app does all of this itself with status200_create_upload_link and status200_finish_upload (see Connect an AI app).
Paid plans
From Beginner. A Free account is refused with 403 plan_limit_reached before anything is reserved: upload the file in the dashboard (Media), which works on every plan.
A daily budget
One per account and UTC day, shared by every upload link (from any API key or AI app) and the URL imports of connected AI apps: Beginner 2 GB, Apprentice 10 GB, Skilled 25 GB, Expert and Agency 100 GB. It resets at 00:00 UTC. Uploading in the dashboard has no daily budget.
Videos
MP4, MOV and WebM (.mp4, .m4v, .mov, .webm; an .m4v is stored as MP4), up to 5 GB (5,368,709,120 bytes).
Images
JPEG, PNG and WebP (.jpg, .jpeg, .png, .webp), up to 20 MB (20,971,520 bytes). The extension of file_name decides the type, and the check reads the first bytes too: renaming a file does not change what it is.
How it works
- 1. Measure the file: its exact size in bytes and its SHA-256. Windows PowerShell:
(Get-Item -LiteralPath 'C:\Videos\EP01.mp4').Lengthand(Get-FileHash -Algorithm SHA256 -LiteralPath 'C:\Videos\EP01.mp4').Hash. macOS:stat -f %z EP01.mp4andshasum -a 256 < EP01.mp4. Linux, WSL or Git Bash:stat -c %s EP01.mp4andsha256sum < EP01.mp4. Hashing a large video takes a while. - 2.
POST /api/v2/media/uploadswithfile_name,size_bytesandsha256(andshellfor a ready-made command). The answer is 201 with afile_id, the upload addressupload.urland its tokenupload.token. - 3. Send the file: run
uploader.command(no code to write), or use any TUS 1.0.0 client. - 4.
POST /api/v2/media/uploads/{file_id}/complete: the file that arrived is checked (exact size, type, first bytes, SHA-256). 200"ready": it is in your media library. 202"verifying": askGET /api/v2/media?file_id=every 15 seconds until it is ready (a 5 GB file can take about 15 minutes), and never upload it again: it finishes on its own. - 5. Post it:
"mediaID": ["<file_id>"]inPOST /api/v2/posts, like an imported file.
Request Body (POST /api/v2/media/uploads)
| Parameter | Type | Description |
|---|---|---|
file_namerequired | string | The file's name, for example EP01.mp4 (a path works: folders are cut off and never stored). Its extension decides the type; a name over 120 characters is shortened with its extension kept |
size_bytesrequired | integer | The exact size in bytes: videos up to 5 GB (5,368,709,120 bytes), images up to 20 MB. The file that arrives must be exactly this size |
sha256required | string | The file's SHA-256: 64 hex characters, either case. The file that arrives must have exactly this hash |
shell | string | powershell (Windows PowerShell) or sh (sh, bash or zsh: macOS, Linux, WSL, Git Bash): the answer then carries uploader.command for that shell. Without it, no uploader block: send the file with your own TUS client |
runner | string | native (our PowerShell or sh uploader) or node (our Node.js uploader, Node.js 18 or newer)Default: native |
max_run_seconds | integer | How long one run of the uploader may take, 30 to 7,000. It always ends within that plus 10 seconds, so run the command with a timeout at least 60 seconds longerDefault: 100 |
curl -X POST 'https://status200uploads.com/api/v2/media/uploads' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer rl_your_api_key' \
-d '{"file_name": "EP01.mp4", "size_bytes": 295698432, "sha256": "9f2c4b6a8e0d1c3b5a7f9e1d3c5b7a9f0e2d4c6b8a0f1e3d5c7b9a1f3e5d7c9b"}'upload.url is the TUS address on storage's own host, upload.token its token (about 2 hours, this file only: keep it out of logs and chat messages). resume_before says until when a stopped upload can be continued; after that, what was sent is deleted. The same file asked for again from the same API key while its link is open answers 200 with "renewed": true: the same address with a fresh token, nothing reserved twice.{
"success": true,
"file_id": "6b1d2f0e-3c4a-4b5d-8e6f-7a8b9c0d1e2f",
"status": "uploading",
"file_name": "EP01.mp4",
"size": 295698432,
"type": "video/mp4",
"sha256": "9f2c4b6a8e0d1c3b5a7f9e1d3c5b7a9f0e2d4c6b8a0f1e3d5c7b9a1f3e5d7c9b",
"renewed": false,
"upload": {
"protocol": "tus-1.0.0",
"url": "https://yglvofckrdutesfzxwyb.storage.supabase.co/storage/v1/upload/resumable/sign/bWVkaWEtdXBsb2Fkcy84ZjE0ZTQ1Zi1jZWVhLTQ2N2EtOWEzNi0xZDViMGUzYTdjMjEvMGU5ZDhjN2ItNmE1Zi00ZTNkLTljMmItMWEwZjllOGQ3YzZiLm1wNC80ZjNlMmQxYy0wYjlhLTQ4NzYtOTU0My0yMTBmZWRjYmE5ODc",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvd25lciI6IjhmMTRlNDVmLWNlZWEtNDY3YS05YTM2LTFkNWIwZTNhN2MyMSIsInVybCI6Im1lZGlhLXVwbG9hZHMvOGYxNGU0NWYtY2VlYS00NjdhLTlhMzYtMWQ1YjBlM2E3YzIxLzBlOWQ4YzdiLTZhNWYtNGUzZC05YzJiLTFhMGY5ZThkN2M2Yi5tcDQiLCJ1cHNlcnQiOmZhbHNlLCJpYXQiOjE3OTEyODgwMDAsImV4cCI6MTc5MTI5NTIwMH0.Qx9_-Qx9_-Qx9_-Qx9_-Qx9_-Qx9_-Qx9_-Qx9_-Qx9_-",
"chunk_size": 6291456,
"token_expires_at": "2026-10-06T14:00:00.000Z",
"upload_expires_at": "2026-10-07T12:00:00.000Z",
"resume_before": "2026-10-06T16:00:00.000Z"
},
"next_step": "Send the file to upload.url with a TUS 1.0.0 client (header x-signature: upload.token; 6291456-byte chunks from the offset a HEAD returns). Then POST /api/v2/media/uploads/6b1d2f0e-3c4a-4b5d-8e6f-7a8b9c0d1e2f/complete. A stopped upload can be continued (after a fresh token, once this one expired) until upload.resume_before (2026-10-06T16:00:00.000Z); after that, what was sent is deleted."
}Our uploader: no code to write
Send shell (powershell or sh) and the answer adds an uploader block: shell, runner, version, download_url, sha256, max_run_seconds and command. The command downloads our uploader into a fresh folder, refuses to run it unless its SHA-256 is the published one, sends the file and removes the folder. Run it as it is in that shell (never inside another shell), with <PATH> in its S200_FILE line replaced by the file's full path, and with a command timeout of at least max_run_seconds + 60 seconds. It needs to reach status200uploads.com and yglvofckrdutesfzxwyb.storage.supabase.co. For "shell": "sh", "max_run_seconds": 540 it looks like this:
mktemp -d | { IFS= read -r d || exit 1
u="$d/s200-upload.sh"
curl -fsSL --proto '=https' --connect-timeout 15 --max-time 45 -o "$u" 'https://status200uploads.com/uploader/v1/s200-upload.sh' || { echo 'Could not download the uploader from status200uploads.com.'; rm -rf "$d"; exit 1; }
{ sha256sum < "$u" 2>/dev/null || shasum -a 256 < "$u"; } | cut -c1-64 | grep -qx 'e9eec70d103b51129a9a072e5ccf838e9ef68b70008d18b07bfc58740359ddbc' || { echo 'The uploader did not download correctly. Do not run it.'; rm -rf "$d"; exit 1; }
S200_UPLOAD_URL='https://yglvofckrdutesfzxwyb.storage.supabase.co/storage/v1/upload/resumable/sign/bWVkaWEtdXBsb2Fkcy84ZjE0ZTQ1Zi1jZWVhLTQ2N2EtOWEzNi0xZDViMGUzYTdjMjEvMGU5ZDhjN2ItNmE1Zi00ZTNkLTljMmItMWEwZjllOGQ3YzZiLm1wNC80ZjNlMmQxYy0wYjlhLTQ4NzYtOTU0My0yMTBmZWRjYmE5ODc' \
S200_UPLOAD_TOKEN='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvd25lciI6IjhmMTRlNDVmLWNlZWEtNDY3YS05YTM2LTFkNWIwZTNhN2MyMSIsInVybCI6Im1lZGlhLXVwbG9hZHMvOGYxNGU0NWYtY2VlYS00NjdhLTlhMzYtMWQ1YjBlM2E3YzIxLzBlOWQ4YzdiLTZhNWYtNGUzZC05YzJiLTFhMGY5ZThkN2M2Yi5tcDQiLCJ1cHNlcnQiOmZhbHNlLCJpYXQiOjE3OTEyODgwMDAsImV4cCI6MTc5MTI5NTIwMH0.Qx9_-Qx9_-Qx9_-Qx9_-Qx9_-Qx9_-Qx9_-Qx9_-Qx9_-' \
S200_CHECK='be11386c' \
S200_MAX_SECONDS='540' \
S200_FILE='<PATH>' \
sh "$u" </dev/null; c=$?; rm -rf "$d"; exit $c; }Inside the single quotes of the S200_FILE line, write each ' of the path as '\'' in sh, and twice in PowerShell (there the typographic quotes ‘ ’ ‚ ‛ too). A run prints progress N% (X.X of Y.Y MiB) every 15 seconds and ends with one JSON line with its state. Its exit code says what to do:
| Exit | state | What to do |
|---|---|---|
| 0 | done | The whole file is stored. Now POST .../complete. |
| 1 | failed | It gave up after retries (network). Run the same command once more; if it ends with 1 again, a firewall or a sandbox may be blocking the storage host. The command itself also ends with 1, with no JSON line, when it stops before the uploader runs: "Could not download the uploader from status200uploads.com." (the same: run it once more), "The uploader did not download correctly. Do not run it." (stop: running it again cannot help), "The uploader could not be checked on this computer (Get-FileHash did not work), so it was not run. Do not run it unchecked." (Windows PowerShell only; stop: running it again in the same PowerShell cannot help), "Node.js was not found on this computer. Ask for the link again with runner native." or "Windows PowerShell (powershell.exe) was not found on this computer. Ask for the link again with runner node (Windows) or with shell sh (macOS, Linux)." (do as it says; a fresh token with that runner or shell works too). |
| 2 | refused | A rerun cannot help as it is; the message says why: the file was not found, is empty, changed or got shorter during the upload (measure it again and make a new link), or storage refused it (POST .../complete once: it says whether the whole file arrived). |
| 3 | partial | This run used up its time (max_run_seconds). Run the same command again: it continues where it stopped. |
| 4 | token_rejected | The token expired or was refused. Get a fresh one (POST .../token with the same shell) and run the new command: it continues where it stopped. |
| 5 | busy | Another uploader on this computer is already sending this file. Wait until it has ended, then run the command again. |
| 6 | bad_command | The command was not copied exactly. Copy it again and change only <PATH>. |
Your own TUS client
Without shell, send the file to upload.url with any TUS 1.0.0 client (the Python tab above is one). HEAD the address for its Upload-Offset, then PATCH pieces of exactly chunk_size bytes (6,291,456, 6 MiB; the last one shorter) from that offset, each with the headers x-signature: <upload.token>, Tus-Resumable: 1.0.0, Upload-Offset and Content-Type: application/offset+octet-stream. After a failed PATCH, HEAD again and go on from the offset it gives. The token goes in that header only, never in a URL. The upload was created by us with the file's exact length and type, so a client never creates one.
{
"success": true,
"file_id": "6b1d2f0e-3c4a-4b5d-8e6f-7a8b9c0d1e2f",
"status": "ready",
"file_name": "EP01.mp4",
"size": 295698432,
"type": "video/mp4",
"sha256": "9f2c4b6a8e0d1c3b5a7f9e1d3c5b7a9f0e2d4c6b8a0f1e3d5c7b9a1f3e5d7c9b",
"message": "EP01.mp4 arrived complete (295,698,432 bytes, SHA-256 matches). Publish it with this file_id."
}{
"success": true,
"file_id": "6b1d2f0e-3c4a-4b5d-8e6f-7a8b9c0d1e2f",
"status": "verifying",
"file_name": "EP01.mp4",
"size": 295698432,
"type": "video/mp4",
"sha256": "9f2c4b6a8e0d1c3b5a7f9e1d3c5b7a9f0e2d4c6b8a0f1e3d5c7b9a1f3e5d7c9b",
"message": "The file arrived and is being checked (size, type and SHA-256). Poll GET /api/v2/media?file_id=6b1d2f0e-3c4a-4b5d-8e6f-7a8b9c0d1e2f every 15 seconds until it is \"ready\". A 5 GB file can take about 15 minutes. It finishes on its own: do not upload it again.",
"retry_after_seconds": 15
}Status, fresh tokens, cancelling
- -
GET /api/v2/media?file_id=answers for an upload too:"uploading"(not all of the file has arrived),"verifying"(it arrived and is being checked),"ready"(with itspublic_url, like any library file) or"failed"(with the reason inerror). Before it is ready there is nopublic_url. - -
POST /api/v2/media/uploads/{file_id}/tokengives the same address a fresh token when the old one expired before the whole file was sent (the uploader's exit 4): sendshell(andrunner,max_run_seconds) for a new command, or no body. Only the API key that made the link, at most 12 times and within 24 hours of the link. - - A stopped upload can be continued until
upload.resume_before: 2 hours after its newest token expired, at most 24 hours after the link was made. After that the upload is closed and what was sent is deleted; make a new link. - -
DELETE /api/v2/media/uploads/{file_id}cancels an upload that is not ready (200"cancelled"): what arrived is deleted within a few minutes. Bytes that arrived still count in the day's budget. A ready upload is a media library file: delete it there. - - Per account: 10 uploads open at once (a link that is never used closes on its own about 4 hours after it was made), 20 new links in 10 minutes and 100 a day, and 60 upload-link calls a minute (new links, tokens, complete and cancel together). A link holds its size of the day's budget until it closes; bytes that arrived still count when the check refuses them.
- - Every answer comes within about 22 seconds and carries
Cache-Control: private, no-store. Refusals have the shape{"error": {"code", "message", "next_step", "retry", ...}}:next_stepsays what to do next, andretry(when there is one) is"never"(the same request cannot succeed now) or"after_reset"(after 00:00 UTC); a wait is inRetry-Afteranderror.retry_after_seconds. A client that follows the OpenAPI file'sx-status200-retrytable reads itsby_operationfirst: a paused check (503upload_links_pausedon complete, which keeps the file) is waited out and asked again.
{
"error": {
"code": "plan_limit_reached",
"message": "Uploading a file from a computer needs a paid plan (Beginner or higher), and this account is on the Free plan.",
"next_step": "Upload the file and make the post at status200uploads.com → Media, which every plan can use, or upgrade: https://status200uploads.com/pricing.",
"retry": "never",
"plan": "free",
"upgrade_url": "https://status200uploads.com/pricing"
}
}{
"error": {
"code": "import_budget_exhausted",
"message": "EP01.mp4 is 1.1 GB, and only 819.2 MB of today's 2 GB budget for files your apps upload or import is left on the Beginner plan. It resets at 00:00 UTC (in 12 h). 282 MB of it is reserved by upload links of this account whose file has not fully arrived; if they are not continued, that is free again by 13:51 UTC.",
"next_step": "To post it now, upload it at status200uploads.com → Media (no daily budget there), or upgrade for a larger budget. Otherwise wait for 00:00 UTC.",
"retry": "after_reset",
"retry_after_seconds": 43200,
"used_bytes": 1288490188,
"held_bytes": 295698432,
"held_until": "2026-10-06T13:51:00.000Z",
"limit_bytes": 2147483648,
"remaining_bytes": 858993460,
"requested_bytes": 1182793728,
"window": "day",
"resets_at": "2026-10-07T00:00:00.000Z",
"plan": "beginner",
"upgrade_url": "https://status200uploads.com/pricing"
}
}| Status | Code | What it means, what to do |
|---|---|---|
| 400 | invalid_request | A field is missing or wrong (the message names it): file_name, size_bytes (the exact size, above 0), sha256 (64 hex characters), shell, runner or max_run_seconds; or file_id sent on a new link, or a file_id in the path that is not a UUID. Fix it and send it again. |
| 401 | unauthorized | No API key, or one that is not active. Send it as Authorization: Bearer rl_your_api_key. |
| 403 | plan_limit_reached | Uploading from a computer needs a paid plan, and this account is on Free: error.plan and error.upgrade_url. Upload the file in the dashboard (Media), which works on every plan, or upgrade. |
| 403 | account_being_deleted | The account is being deleted: nothing can be uploaded to it. |
| 404 | media_not_found | No upload with that id on your account (for a fresh token: none made by this API key). Make a new link. |
| 405 | method_not_allowed | A method the path does not take (the Allow header names the one it does). An upload's status is GET /api/v2/media?file_id=. |
| 409 | upload_already_received | The whole file has already arrived (error.file_id): POST .../complete instead of uploading it again or asking for a token. |
| 409 | upload_closed | The upload was cancelled; or, on DELETE, it is ready (delete it in the media library instead) or already closed. Make a new link to send the file again. |
| 409 | upload_incomplete | Storage has not received the whole file. Run the uploader (or your client) again, it continues where it stopped, then complete. Twice in a row right after exit 0: cancel the upload and make a new link. |
| 409 | upload_being_checked | DELETE while the file is being checked: wait for Retry-After (60), or let the check finish. |
| 410 | upload_expired | It was not finished in time (24 hours after the link, or 2 hours after its last token expired without the whole file) and was removed with what was sent; or a ready file has since left the media library (deleted, or older than 7 days). Make a new link. |
| 413 | media_too_large | Larger than the largest file (error.size_bytes, error.max_bytes). Use a smaller file. |
| 415 | unsupported_media_type | The extension is not one of the types taken. Convert the file (a video to MP4, an image to JPEG) and make a new link. |
| 422 | upload_rejected | The file that arrived is not the one announced, and it was deleted (error.reason). sha256 or size: measure the file again and make a new link with the new values (when they are the same, do not upload it again). type or content: it is not really a file of its type; convert it. replaced, conflict or gone: make a new link. |
| 422 | upload_failed | It could not be checked within 24 hours because of a problem on our side (error.reason unverifiable) and was deleted. We were alerted. Make a new link. |
| 429 | import_budget_exhausted | The file does not fit what is left of today's budget (error.used_bytes, limit_bytes, remaining_bytes, requested_bytes, resets_at; held_bytes and held_until when unfinished links of this account hold part of it). It frees at 00:00 UTC (error.retry_after_seconds). A file larger than the whole day's budget has no retry_after_seconds: waiting does not help. Upload it in the dashboard (no daily budget there), or upgrade. |
| 429 | too_many_open_uploads | This account has the most uploads open at once (error.open, error.limit). Complete or cancel one, then ask again after Retry-After (60). A link that is never used closes on its own about 4 hours after it was made. |
| 429 | rate_limited | Too many new links in 10 minutes or today (error.window), or too many upload-link calls this minute. Wait for Retry-After (also error.retry_after_seconds), then send it again. |
| 429 | renewals_exhausted | The link has had its 12 fresh tokens, or it is older than 24 hours. Make a new link. |
| 500 | server_error | Our side failed and nothing was handed out. Send it again in a minute: the same file from the same API key finds its open link, so nothing is reserved twice. |
| 503 | upload_links_paused | Paused for maintenance: making links, or (on complete) checking files. A file that arrived is kept: ask complete again later (Retry-After 600) and do not upload it again. A new file: upload it in the dashboard (Media) for now; never put it on a public site to import it. On a new link or a fresh token it can also mean that uploading from a computer is switched off for this one account (the message says "switched off for this account"): asking again does not help, so upload the file in the dashboard (Media), or write to info@status200uploads.com. |
| 503 | uploads_busy | Too many uploads are running, or something on our side could not answer. Nothing was reserved: send it again after Retry-After (30, 60 or 600 seconds). |
The uploader's published versions
The uploader is code that runs on your computer, so every version is published at a fixed address under https://status200uploads.com/uploader/vN/ (served as text/plain), never changes once published (a new uploader is a new version), and is checked by its SHA-256 before it runs: the command carries the hash and refuses any other download. Anyone can check a copy against this list, for example curl -fsSL https://status200uploads.com/uploader/v1/s200-upload.sh | sha256sum (macOS: shasum -a 256), or in PowerShell (Get-FileHash -Algorithm SHA256 -LiteralPath .\s200-upload.ps1).Hash (it prints upper case). New links use v1.
| Version | File | SHA-256 |
|---|---|---|
| v1 | https://status200uploads.com/uploader/v1/s200-upload.ps1 | 56561624b27af9d84038c164025ae159427b19e4e23cc8ecc017452378dd5041 |
| v1 | https://status200uploads.com/uploader/v1/s200-upload.sh | e9eec70d103b51129a9a072e5ccf838e9ef68b70008d18b07bfc58740359ddbc |
| v1 | https://status200uploads.com/uploader/v1/s200-upload.mjs | 6fef8b6181fba3bf069861e69e4700c83a292fa468a3201b37a58c2f3944a335 |
What is kept and logged
- - A ready file is a media library file like any other: kept 7 days (and until a scheduled post that uses it goes out), and, like every library file, public at its exact address while it is kept, even when the post itself is private. Until it is ready it is in a private place, and a refused file is deleted.
- - Your API request log (Dashboard → API) has one row per answer with a summary: the ids, the status, the type and size, the uploader's shell, runner and version, and the times. Never the token, the upload address or the command.
- - The token works only for this one file, only to upload it, and for about 2 hours. Where it goes is up to you: an AI app's chat, a command line and the process list of your computer can show it.
/api/v2/media/filesThe files in your media library, newest first, page by page: what you uploaded in the dashboard, imported from a URL or sent through an upload link, each with its file_id.
Full URL: GET https://status200uploads.com/api/v2/media/files. Use it to find a file you uploaded in the dashboard (Media) and post it by its file_id in content.mediaID, instead of putting it anywhere public to import it. Every plan, Free included. Each file says whether it can be posted now (usable) and when it may be removed (expires_at: 7 days after upload, or later while a waiting scheduled post uses it). No file's address is ever answered here: GET /api/v2/media?file_id= gives one file's public_url.
- - The same API key as for posting, in
Authorization: Bearer rl_your_api_key. You only ever see your own account: another account's id reads exactly like an id that does not exist. - - On
https://status200uploads.com/api/v2only. The older URL answers a GET with 405. - - 60 reads a minute per account, all your API keys together (posting options also 10 a minute, see Rate Limits). Over the limit: 429
rate_limitedwith aRetry-Afterheader anderror.retry_after_seconds. Reads are not counted in your API key's request count, so a key used only for reads shows 0 requests on the API page: it is still in use. - - Answers carry
Cache-Control: private, no-store: do not cache them between API keys.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
name_contains | string | Files whose name holds every word of it, ignoring capital letters and punctuation (1 to 120 characters): "beach day" finds Beach_Day-final.mp4 |
type | string | image (JPEG, PNG, WebP) or video (MP4, MOV, WebM) |
since | string | Only files that arrived at or after this time: a date and time with a time zone, such as 2026-10-08T14:05:00Z (the now of an earlier answer). It is read 2 minutes earlier to allow for clocks, so answers overlap: skip a file_id you already handled |
limit | integer | Files per page, 1 to 100. Default 20 |
cursor | string | next_cursor from the previous page, exactly as it came. A cursor that is not ours is refused with 400 |
include_unconfirmed | boolean | true or false (default false). Files that need the account holder's confirmation before they may be posted (confirm_before_posting true) are left out unless it is true. No file needs one today |
curl 'https://status200uploads.com/api/v2/media/files?name_contains=beach&type=video' \
-H 'Authorization: Bearer rl_your_api_key'uploaded_at. status is "ready", "processing" (an import still copying), "verifying" (an upload being checked) or "failed" (with the reason in error); usable is true when it can be posted now. source is "dashboard", "url_import" or "upload_link". file_name is the name it was uploaded or imported with, cleaned (no control characters, at most 120 characters): show it, never run it. now is when this was read: send it as since next time to see what arrived after it. One request reads at most 500 files of the library, so a page can have fewer than limit and still a next_url: follow it until it is null. An unknown parameter is named in warnings (code unknown_parameter) and not used.{
"data": [
{
"file_id": "6b1d2f0e-1111-4222-8333-444444444444",
"file_name": "Beach day.mp4",
"type": "video/mp4",
"kind": "video",
"size": 48213377,
"status": "ready",
"usable": true,
"source": "dashboard",
"uploaded_at": "2026-10-07T12:08:20.740Z",
"expires_at": "2026-10-14T12:08:20.740Z",
"confirm_before_posting": false,
"error": null
}
],
"next_cursor": null,
"next_url": null,
"now": "2026-10-07T12:31:02.118Z"
}| Status | Error | Description |
|---|---|---|
| 400 | Bad Request | bad_request with error.parameter (name_contains, type, since, limit, cursor or include_unconfirmed) and a message saying what it takes |
| 405 | Method Not Allowed | method_not_allowed: /api/v2/media/files answers GET only (Allow: GET, OPTIONS) |
| 401 | Unauthorized | unauthorized: missing or invalid API key (the same answers as for a POST) |
| 429 | Too Many Requests | rate_limited: this account sent 60 reads in this minute, all its API keys together (error.limit 60), or asked for posting options 10 times (error.limit 10). Wait for the Retry-After seconds (also in error.retry_after_seconds), then send it again |
| 500 | Server Error | server_error: it could not be read just now, and nothing changed. Try again in a minute |
Guides
Your First API Call
This guide walks you through making your first successful API call to publish a post.
Get your API key
Sign in to your dashboard, navigate to API Management, and click "Generate Key". Copy the key - you will only see it once.
Connect a social account
Go to Connections and link at least one social media account. Note the account handle (e.g. @myprofile) - you will need this for the API call.
Upload your media
curl -X POST 'https://status200uploads.com/api/v2/media' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer rl_YOUR_KEY' \
-d '{"url": "https://example.com/video.mp4"}'Save the file_id from the response. If the status code is 202, the import is still running: request GET https://status200uploads.com/api/v2/media?file_id=YOUR_FILE_ID every 10 to 30 seconds until status is "ready".
Publish your post
curl -X POST 'https://status200uploads.com/api/v2/posts' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer rl_YOUR_KEY' \
-d '{
"post": {
"accountId": "@myprofile",
"platform": "tiktok",
"content": {
"text": "Hello world! #firstpost",
"mediaID": ["YOUR_FILE_ID"]
},
"tiktok": { "privacyLevel": "PUBLIC_TO_EVERYONE" }
}
}'Check the result
A successful response has status 200 and the network's result in a data object. For TikTok it contains "status": "processing" and a post_id, because TikTok finishes processing the video after the response. The final result is shown on your History page in the dashboard. A 202 with "queued": true means the daily allowance of the profile (Beginner, Apprentice) was used up and the post was queued for the next day; on Free, a 429 monthly_limit_reached means this month's 30 posts are used. A 202 with "code": "still_publishing" means TikTok took longer than the request can wait (about 25 seconds): the result appears in History, so do not send the request again. A 409 media_processing means the import from step 3 is not finished yet: wait 30 seconds and send the same request again.
Posting to Several Networks
Publish the same content to several networks: import the media once, then send one request per network. Each request publishes to one network from one profile.
- - The file has to fit every network you send it to. A network whose limit is lower answers 413
MEDIA_TOO_LARGEand the other posts are not affected. See platform file limits. - - Requests are throttled per network: two posts to the same network need 20 seconds between them, even from different profiles (429 otherwise, with a
Retry-Afterheader). One post to each of several networks can be sent back to back. Skool accepts one post per hour and 10 per day. - - The post allowance counts successful posts, all networks combined. Once a daily allowance (Beginner, Apprentice: per profile per day) is used up, requests are answered 202
queued_for_next_dayand publish on the next day; on Free, 429monthly_limit_reachedafter 30 posts in a month. - - A post that uses a
file_idwhose import is still running is answered with 409media_processingafter about 12 seconds. Wait for the import, then send the post again. Afile_idthat no longer exists (files are kept for 7 days, and a file a scheduled post uses until that post goes out) is answered with 404media_not_found: import the file again. - - A 202 with
still_publishingmeans the network is slower than the request can wait. The post is on its way: do not send it again (with the sameIdempotency-Keyyou only get this answer back). - - Make one
Idempotency-Keyper post, once, outside the retry loop, and send it with every try: a timeout, a lost connection, a gateway's 5xx (noerror.codeof ours), a 409idempotency_in_progressand the 503s that say nothing was sent can then be retried without posting twice (see Safe Retries). After any other 5xx, read the message and check History before sending it again.
import time
import uuid
import requests
API = "https://status200uploads.com/api/v2"
HEADERS = {
"Content-Type": "application/json",
"Authorization": "Bearer rl_your_api_key"
}
# One entry per network, with the options of that network
targets = [
{"platform": "tiktok", "tiktok": {"privacyLevel": "PUBLIC_TO_EVERYONE"}},
{"platform": "youtube", "youtube": {"privacyStatus": "public"}},
{"platform": "facebook", "facebook": {"postType": "video"}},
{"platform": "linkedin"},
]
# 1. Import the video once and wait until it is ready
media = requests.post(
f"{API}/media",
headers=HEADERS,
json={"url": "https://example.com/content.mp4"}
).json()
file_id = media["file_id"]
while media["status"] == "processing":
time.sleep(15)
media = requests.get(
f"{API}/media", headers=HEADERS, params={"file_id": file_id}
).json()
if media["status"] != "ready":
raise SystemExit(media.get("error") or "Import failed")
# 2. One request per network
def publish(target):
body = {
"post": {
"accountId": "@myprofile",
"content": {
"text": "Check out our latest content!",
"mediaID": [file_id]
},
**target
}
}
# One Idempotency-Key per post, made once: every try below sends the same key, so a post
# that went out is answered again (Idempotent-Replayed: true), never posted twice.
headers = {**HEADERS, "Idempotency-Key": str(uuid.uuid4())}
status, data = 0, {"error": {"message": "no answer"}}
for _ in range(5):
try:
# Every answer comes within about 25 seconds
resp = requests.post(f"{API}/posts", headers=headers, json=body, timeout=60)
except (requests.Timeout, requests.ConnectionError):
time.sleep(5)
continue # safe with the same key
status = resp.status_code
try:
data = resp.json()
except ValueError:
data = {} # a gateway's page, not ours
error = data.get("error") if isinstance(data.get("error"), dict) else {}
code = error.get("code")
wait = int(resp.headers.get("Retry-After", "5"))
if status == 409 and code in ("media_processing", "idempotency_in_progress"):
time.sleep(wait) # import still running, or the first try still working: nothing new was sent
elif status == 429 and code == "rate_limited":
time.sleep(wait) # throttled: wait, then send again (other 429s: stop)
elif code in ("idempotency_unavailable", "caller_check_unavailable", "upstream_unavailable") or (status >= 500 and not code):
time.sleep(wait) # nothing was sent, or a gateway's page: safe with the same key
else:
break # any other answer, another 5xx included: read it and check History first
return status, data
for target in targets:
status, data = publish(target)
if status == 202 and data.get("code") == "scheduled":
print(target["platform"], "scheduled for", data["scheduled_at"]) # never resend without its key
elif status == 202 and data.get("queued"):
print(target["platform"], "queued for", data["scheduled_at"])
elif status == 202 and data.get("code") == "still_publishing":
print(target["platform"], "still publishing: see History") # never resend without its key
elif status in (200, 202):
print(target["platform"], "accepted")
else:
print(target["platform"], "failed:", status, data.get("error"))Handling Media Uploads
Best practices for uploading images and videos through the API.
Supported Formats
Images
JPEG, PNG, WebP - up to 20 MB
Videos
MP4 (M4V too), MOV, WebM - up to 5 GB
These are the media library's limits, the same for both ways in below. Other formats (GIF, HEIC, SVG, MKV ...) are refused: convert them first. Each network accepts less than 5 GB for publishing: see platform file limits.
TikTok takes photos up to 1080 × 1920 pixels: a larger JPEG is sent as a smaller copy and the answer says so, and what we cannot make smaller is refused before anything is sent. See TikTok Photos.
Two Ways In
- - A file at a public URL (S3, Cloudflare R2, any CDN): import it with
POST /api/v2/media, on every plan. The steps are below. - - A file on a computer: upload it through a one-time upload link,
POST /api/v2/media/uploads(paid plans, within a daily budget): your script, or our small uploader, sends it straight to storage, and it is checked before it can be posted. See Upload from a computer. Never put a file on a public site only to import it from there.
Importing From a URL
- 1. Host your media on a publicly accessible URL (S3, CloudFlare R2, any CDN). The URL must return the file itself: a web page that shows the file, or a file in another format, is refused with 415. A server that sends no type (or a generic one) has the file judged by its first bytes
- 2. Call
POST /api/v2/mediawith the URL - 3. A small file is ready when the request returns (200,
"status": "ready"). A large file answers 202 with"status": "processing"and is imported in the background - 4. After a 202, request
GET /api/v2/media?file_id=...every 10 to 30 seconds until the status is"ready". The answer includesprogressfrom 0 to 100 - 5. Use the
file_idwhen creating posts. A post sent before the import is ready is answered with 409media_processing - 6. A single
file_idcan be reused across multiple posts and platforms. Files are kept for 7 days; a file used by a scheduled post is kept until that post publishes. After that, a post that uses thefile_idis answered with 404media_not_found
Hosting large files
A large import continues automatically after an interruption when the source server supports HTTP Range requests, which S3, Cloudflare R2, Supabase Storage and most CDNs do. A source without Range support still works, but a very large file cannot be continued after a dropped connection.
Pro Tip
For Instagram and Threads carousels and TikTok photo slideshows, import each file and pass all file_id values in the mediaID array. Imports are throttled to one every 20 seconds.
TikTok Photos
TikTok takes photos up to 1080 × 1920 pixels (1920 × 1080 for a wide photo, 1080 × 1080 for a square one). It accepts a post with a larger photo and then refuses it a few seconds later, so we deal with the size before anything is sent, and we always tell you what we did. This works the same way from the dashboard, a scheduled post, the API and an AI app. JPEG and WebP photos within that size are sent as they are.
A larger JPEG: we send a smaller copy
We make a JPEG copy that fits inside TikTok's size (at least 90% of the largest size that fits), turned the way the photo is shown, and send that copy to TikTok. Your original file is never changed. The copy is a separate file in your media library, named after your file ("IMG_0042.JPG" gets "IMG_0042 (smaller copy for TikTok).jpg"), kept for 7 days like every file there. You are told, with both sizes:
Photo too large for TikTok: it is 2160 × 2700 pixels and TikTok takes up to 1080 × 1920, so we sent a smaller copy (1080 × 1350). Your original file is unchanged.
In a post with several photos, the sentence names the photo:
Photo 2 of 3 too large for TikTok: it is 4032 × 3024 pixels and TikTok takes up to 1920 × 1080, so we sent a smaller copy (1440 × 1080). Your original file is unchanged.
- - Dashboard, Post now: on the TikTok card of the preview and in a message right after posting.
- - History: on the post, and in its details under "What we changed", whether it came from the dashboard, a scheduled post, the API, an AI app or Retry. The calendar's post details show it too.
- - API: in the answer's
warnings, coderesized_for_tiktok(see below), and in the post'snoticeswhenever you read it back (GET /api/v2/posts,GET /api/v2/posts/{id}). The post went out. - - AI apps: the publish and retry answers carry the same warning and ask the AI to tell you, and so does reading the post back (
status200_get_post,status200_list_posts). - - Scheduled posts: the photo is checked when the post goes out, not when you schedule it, so the notice is on the post from then on: in History, when the API reads the post or the scheduled post back (each network's result has its
notices), and when an AI app does.
What we cannot make smaller is refused before anything is sent
Nothing goes to TikTok, nothing is published, and you are told why and what to send instead:
Photo too large for TikTok: it is 8064 × 6048 pixels, too large for us to make a smaller copy. Save it smaller (for example 1920 × 1080) and post again. Nothing was sent.
Photo too large for TikTok: it is a WebP of 2160 × 2700 pixels, and we cannot make a smaller copy of a WebP. Save it as a JPEG of at most 1080 × 1920 and post again. Nothing was sent.
This happens to a photo larger than 1080 × 1920 that is:
- - a WebP: we cannot make a smaller copy of a WebP file
- - a JPEG over 5 MiB, or over about 13 megapixels (a typical 12-megapixel phone photo is fine)
- - a progressive JPEG (a kind that web and editing apps often save) over 2 MiB, or over about 12 megapixels
- - a JPEG that keeps more colour detail, at fewer megapixels: about 10 with 4:2:2 colour (some cameras), about 6.8 with full colour detail (4:4:4); progressive about 9.4 and 6.3
- - a photo under about 1.8 times the size of its copy on each side, which we read at full size, at fewer megapixels: about 6, or 3 with full colour detail; progressive about 4, or 2
- - a JPEG we cannot read: CMYK, 12-bit, arithmetic-coded, lossless, hierarchical, or with unusual colour components
What to do: save or export the photo as a JPEG of at most 1080 × 1920 pixels (for example 1080 × 1350 for an upright photo, 1920 × 1080 for a wide one, 1080 × 1080 for a square one) and post again. A JPEG within that size is always sent as it is.
- - Dashboard: on the TikTok card and in a message; the post is listed in History as failed, with these words.
- - API: 422
photo_too_large_to_resize, witherror.photosaying which photo (counting from 1). Nothing is recorded in History. Do not send the same file again. - - AI apps: the same message, and the next step for the AI: ask you for a JPEG of at most 1080 × 1920, and do not send the same file again.
- - Scheduled posts: the post fails when it goes out, and History shows these words.
Rarely, a copy cannot be made, for example from a damaged file (code photo_conversion_failed, a failed post in History):
Photo too large for TikTok: it is 2160 × 2700 pixels, and we could not make a smaller copy of it (the file is incomplete). Save it smaller (for example 1080 × 1350) and post again. Nothing was sent.
When TikTok refuses the size after taking the post
TikTok checks a photo after it has accepted the post. If TikTok refuses the size at that point, History says:
Photo size refused by TikTok: TikTok refused the size of a photo in this post (picture_size_check_failed). TikTok takes photos up to 1080 × 1920 pixels. We now make a smaller copy for TikTok when a JPEG is larger, so publish it again as a new post. If TikTok refuses it again, save the photo as a JPEG of 1080 × 1350 or 1080 × 1920 pixels and post it again.
Retry is not offered for that post (TikTok already has it): publish the photo again as a new post.
Other TikTok photo rules
- - PNG and GIF photos are sent as JPEG copies, because TikTok takes only JPEG and WebP (warning
converted_to_jpeg). One larger than 1080 × 1920 also gets a smaller copy, and the same warning says so with both sizes: "Photo too large for TikTok: it is 1170 × 2532 pixels and TikTok takes up to 1080 × 1920, so the copy is also smaller (887 × 1920)." A PNG or GIF over 6,000,000 pixels is refused (422photo_too_large_to_convert). - - HEIC, AVIF, BMP and TIFF photos are refused (422
photo_format_not_supported): save them as JPEG. - - At most 10 photos of one post get a copy (a JPEG copy or a smaller copy); a post that needs more is refused with what to do. TikTok takes up to 35 photos in a post, each up to 20 MB.
- - TikTok's own documentation says photos may be at most "1080p"; we use 1080 × 1920, and 1920 × 1080 for a wide photo.
For the API and AI apps
- -
resized_for_tiktok(inwarnings): a smaller copy was sent. Besidescodeandmessage, the entry hasphoto(which photo, counting from 1),photos(how many the post has),sizeandcopy_size({"width": 2160, "height": 2700}, as the photo is shown). - -
converted_to_jpeg(inwarnings): a PNG or GIF was sent as a JPEG copy (photo, andfrom: its format). - - 422
photo_too_large_to_resize,photo_too_large_to_convert,photo_format_not_supported: refused before anything was sent, witherror.photo; no post is recorded. The same file will be refused again. - - 422
photo_conversion_failed: the copy could not be made; the post is recorded as failed. - -
picture_size_check_failed: TikTok's own code, in brackets in the post'serror_message(GET /api/v2/posts/{id}) when TikTok refused the size after taking the post. - - The AI connector answers the three photo refusals with
retry"never" and anext_stepasking the person for a JPEG of at most 1080 × 1920, and addstell_the_personto a publish or retry answer whose warnings the person should hear. - - A dry run (
dryRun, or the connector'sstatus200_validate_post) does not read a photo's pixels: a photo larger than 1080 × 1920 is made smaller, or refused, only when it is published. - - Reading a post back repeats the notice, also for a post that went out through a schedule:
GET /api/v2/postsandGET /api/v2/posts/{id}carrynoticeson each post and on each network's result of a scheduled post (the warning'scode,messageand, for a photo,photo(for a smaller copy alsophotos,sizeandcopy_size);[]when there is none, and on the other networks);status200_get_postandstatus200_list_postscarry them aswarnings(code and message), withtell_the_person.
Using Python
The Status 200 Uploads package for Python, status200uploads, is on PyPI. Install it with pip install status200uploads. It needs Python 3.11 or newer. By default it sends an Idempotency-Key with every post. It sends a request again only when the API's retry table allows it, and it can wait until a post is done. Its README, on PyPI and with the source on GitHub, shows how to use it.
- - Put your API key in the
STATUS200_API_KEYenvironment variable, or pass it asStatus200(api_key=...). The package never prints it. - -
publish()returns aResultfor a 200 and for every 202. A 202 is never a failure: the post is on its way, scheduled or queued, so do not send it again.result.codenames the case, for examplescheduled,queued_for_next_dayorstill_publishing(Noneon a 200, and on a 202 while the network is still processing the post). - - A refusal raises
Status200Errorwith the API'scodeandmessage: show the message.OutcomeUnknownmeans no clear answer came back and the post may have gone out: look ats200.list_posts()or History before you send it again. - -
s200.import_media(url)imports a file and waits until it is ready: send itsfile_idin thecontent.mediaIDlist, for example"mediaID": [media["file_id"]].s200.publish(post, scheduled_for=...)schedules a post, ands200.cancel(result.status200.scheduled_post_id)stops it before it goes out.
from status200uploads import OutcomeUnknown, Status200, Status200Error
s200 = Status200() # the key comes from STATUS200_API_KEY
try:
# 1. Your profiles, and the networks connected to each
for account in s200.list_accounts():
print(account["handle"], [network["platform"] for network in account["networks"]])
# 2. Publish, then read the post every 30 seconds until the network is done (10 minutes at most)
post = {"accountId": "@myprofile", "platform": "linkedin", "content": {"text": "Hello from Python"}}
result = s200.publish(post, wait=True)
if result.final: # the post as last read: its status and link
print(result.final.get("status"), result.final.get("permalink"))
else: # scheduled or queued (result.code says which), or it could not be read back
print(result.code)
except OutcomeUnknown as error:
print(error.message) # no clear answer: it may have gone out, check s200.list_posts() first
except Status200Error as error:
print(error.code, error.message) # refused, or a wrong key (unauthorized): the message says whyWithout the package
Any HTTP library works too. The whole API is described in OpenAPI 3.1 at https://status200uploads.com/openapi.yaml, so a client can also be generated from it. The example below uses requests.
- - Make one
Idempotency-Keyper post, once, before any retry, and send it on every try: a timeout, a lost connection or a gateway's page can then be sent again without posting twice (see Safe Retries). - - Send again, after its wait, only what the OpenAPI file's
x-status200-retrytable answerswait_and_resend, and a timeout or an answer that is not JSON once (resend_once_with_key). A 202 is never a failure (the post is on its way, scheduled or queued), and a refusal carriescodeandmessage: show the message. - - Set a timeout of 30 seconds or more: every answer of
/api/v2arrives within about 25 seconds.
import time, uuid, requests
API = "https://status200uploads.com/api/v2"
HEADERS = {"Authorization": "Bearer rl_your_api_key"}
# 1. Your profiles, and the networks that can post now
for profile in requests.get(f"{API}/accounts", headers=HEADERS, timeout=30).json()["data"]:
print(profile["handle"], [n["platform"] for n in profile["networks"] if n["health"] == "ready"])
# 2. Publish, with one Idempotency-Key made once for this post
body = {"post": {"accountId": "@myprofile", "platform": "linkedin", "content": {"text": "Hello from Python"}}}
key = str(uuid.uuid4())
# The codes a publish can get that the OpenAPI file's x-status200-retry table answers
# "wait_and_resend" (all of them but cancel_unconfirmed, a DELETE's): nothing was sent, or the
# first answer comes back
WAIT_AND_RESEND = {"media_processing", "idempotency_in_progress", "rate_limited", "caller_check_unavailable",
"schedule_check_unavailable", "idempotency_unavailable", "dry_run_unavailable",
"upstream_unavailable", "skool_unavailable", "x_unavailable"}
resp, answer, resent_unknown = None, None, False
for attempt in range(5):
try:
resp = requests.post(f"{API}/posts", headers={**HEADERS, "Idempotency-Key": key}, json=body, timeout=40)
answer = resp.json()
except (requests.Timeout, requests.ConnectionError, ValueError):
# No answer, or a gateway's page instead of ours: the table's "not_json" rule,
# resend_once_with_key. The same key makes it safe: a post that went out is answered again.
answer = None
if resent_unknown:
break
resent_unknown = True
time.sleep(5)
continue
error = answer.get("error") if isinstance(answer.get("error"), dict) else {}
wait = int(resp.headers.get("Retry-After") or error.get("retry_after_seconds") or 5)
if error.get("code") in WAIT_AND_RESEND and wait <= 60:
time.sleep(wait)
continue
break # any other answer: read it
# 3. Follow the post until it is done (10 minutes at most)
if answer and "status200" in answer:
for _ in range(20):
post = requests.get(answer["status200"]["status_url"], headers=HEADERS, timeout=30).json()["data"]
if post["done"]:
break
time.sleep(30)
print(post["status"], post.get("permalink"))
else:
print(resp.status_code if resp is not None else "no answer", answer)Integrating with n8n
Use n8n workflows to automate social media publishing with Status 200 Uploads.
The Status 200 Uploads community node, @status200uploads/n8n-nodes-status200uploads, is on npm. On self-hosted n8n, an owner or admin opens Settings, Community Nodes, Install, and enters the package name. In queue mode, install it with npm in ~/.n8n/nodes instead, as the node's README says. Then create a Status 200 Uploads API credential with your API key.
n8n Cloud installs only community nodes that n8n has verified. This node is not verified yet. Until n8n verifies it, use the HTTP Request node on n8n Cloud, as described below. The HTTP Request node works on self-hosted n8n too.
- - Post: Create publishes a post now, schedules it (When: At a Set Time) or only checks it (Check Only (Dry Run)). Post: Get and Get Many read your posts and scheduled posts, and Post: Cancel stops a scheduled or queued post that has not gone out.
- - Account: Get Many lists your profiles and their networks. Account: Get Posting Options gives what a profile may do on each network: TikTok privacy levels, Pinterest boards, Skool groups, the Instagram limit. Media: Import From URL imports a file and waits until it is ready, and Media: Get reads an import.
- - By default every Post: Create and Media: Import From URL sends an automatic
Idempotency-Key, made from the workflow, the execution, the node, the item and the request. With Retry On Fail on, a retry in the same execution sends the same key and gets the first answer back instead of posting twice. Keep$nowand random values out of the node's fields: a changed request is a new post. - - One node sends one post to one network from one profile: to cross-post, use one node per network. The node can also be used as a tool by n8n's AI Agent.
Two example workflows are in the templates folder of the public repository: one cross-posts text and an image to X, LinkedIn and Facebook, the other posts a short video to TikTok, Instagram Reels and YouTube Shorts and waits until each network is done. The node's README describes its fields, its answers and the limits it works within.
With the HTTP Request node
On n8n Cloud until the node is verified, or wherever you prefer plain requests, the HTTP Request node does the same. Every request and answer it uses is described in the OpenAPI file at https://status200uploads.com/openapi.yaml.
Add an HTTP Request node
Set the method to POST and the URL to https://status200uploads.com/api/v2/posts
Configure headers
Add Authorization: Bearer rl_your_key and Content-Type: application/json as headers. Add an Idempotency-Key header too, built from the item and the network, for example {{ $json.id }}-tiktok: the same item always makes the same key, so a retry of this node gets the first answer back instead of posting twice. Never build it from $now or a random value (each retry would then be a new post), and never let it come out empty (that is refused with 400 idempotency_key_invalid). The id part must be there on every item: an item without one would make the key -tiktok, shared by every such item (filter those items out first, or use an id that is always set). A key is remembered for 24 hours.
Set the JSON body
Use the standard post body structure. You can dynamically reference values from previous nodes using n8n expressions like {{ $json.caption }}.
Connect your trigger
Connect any trigger node (Schedule, Webhook, RSS, etc.) to automate when posts are created. For example, use a Schedule node to post every day at 9 AM. To publish at a set time instead of when the workflow runs, put post.scheduledFor in the body with a time zone (for example {{ $json.publishAt }} as 2026-10-01T09:00:00Z): the answer is 202 with "code": "scheduled". Without the Idempotency-Key header n8n must not retry it (it would be scheduled twice); with it, a retry gets the same 202 back and nothing is scheduled again.
Wait until a post is published
Every answer that made a post carries status200.status_url, the address that reads it back. A loop of a few nodes waits for the outcome, stops after 10 minutes, and never sends the post a second time:
HTTP Request: POST the post (name the node Post)
As above, with the node renamed Post (the next steps refer to it by that name). With the Idempotency-Key header, Retry On Fail (Max Tries 5, Wait Between Tries 5000 ms) never posts twice a post whose answer was lost: a retry gets the first answer back, or 409 idempotency_in_progress while the first try is still working. A refusal, or a failure the network reported with its own code, is not remembered: each try sends it again (it is refused again, or goes out once). Keep the body the same on every try: work out times in an earlier node, not with $now in this node's body, or a retry is answered 422 idempotency_key_reused. Without the header, turn Retry On Fail off for this node: a post sent twice is posted twice.
IF: is there a post to follow?
Condition {{ !!$('Post').item.json.status200 }} is true. False: nothing was made (a refusal: read its error.message), or a late answer from X, LinkedIn, Pinterest, Threads, Skool or Instagram came before the post was in History. Do not send it again; it shows in History and in GET /api/v2/posts once the network answers.
HTTP Request: GET its status
Method GET, URL {{ $('Post').item.json.status200.status_url }}, the same Authorization header. Refer to the Post node by name, not to $json: on the second pass this node's input is its own previous answer, which has no status200. The answer's data has status, permalink and done.
IF: done, or waited long enough?
Condition {{ $json.data.done || $runIndex >= 20 }} is true ($runIndex counts this node's passes, so 20 is 10 minutes). True: carry on with data.status and data.permalink. If data.done is still false then, stop anyway: a post still processing (a large video) finishes by itself and shows in History, and a timeout was sent and never confirmed (look on the network, never send it again). False: go to the Wait node.
Wait 30 seconds, then back to step 3
A Wait node of 30 seconds, connected back to the GET. Reads are limited to 60 a minute per account (all your API keys together), so one read every 30 seconds per post leaves plenty of room. For a scheduled post, data.outcome and data.results say what each network did once it has gone out.
To read your whole history, use an HTTP Request node on https://status200uploads.com/api/v2/posts?limit=100 with n8n's pagination: "Response Contains Next URL", next URL {{ $response.body.next_url }}, complete when {{ !$response.body.next_url }}. https://status200uploads.com/api/v2/accounts lists your profiles and their ids for post.accountId.
Integrating with Make.com
Build automated scenarios in Make.com that publish content through the Status 200 Uploads API.
Create a new scenario
Start a new scenario in Make.com and add an HTTP module (Make a request).
Configure the HTTP module
Set URL to https://status200uploads.com/api/v2/posts, method to POST, and body type to JSON. Add your Authorization header, and an Idempotency-Key header mapped from the row and the network (for example the sheet's row id followed by -tiktok), so that a scenario run again posts nothing twice (see Safe Retries).
Map your data
Use Make.com's data mapping to dynamically populate the post body. Connect to Google Sheets, Airtable, or any data source for content.
Schedule and activate
Set the scenario to run on a schedule or trigger from another module. Activate the scenario to start publishing automatically.
Integrating with Zapier
Connect Status 200 Uploads to thousands of apps through Zapier's webhook integration.
Create a new Zap
Choose your trigger app (e.g., Google Sheets, Airtable, or Schedule by Zapier).
Add a Webhooks action
Select "Webhooks by Zapier" as the action and choose "Custom Request".
Configure the request
Set method to POST, URL to https://status200uploads.com/api/v2/posts. Add headers for Authorization and Content-Type, and an Idempotency-Key made from the trigger's record id and the network, so that a replayed Zap posts nothing twice (see Safe Retries). Set the data field to your JSON post body.
Test and publish
Test the Zap with sample data. Once confirmed working, turn on the Zap to automate publishing.
Resources
Changelog
Changes to the API by month, newest first. Breaking changes are marked.
GET /api/v2/media/files lists your media library; share links in POST /api/v2/media are rolling out
- +GET https://status200uploads.com/api/v2/media/files lists the files of your media library, newest first, on every plan: what you uploaded in the dashboard, imported from a URL or sent through an upload link. Parameters: name_contains (every word in the name, 1 to 120 characters), type (image or video), since (a date and time with a time zone, read 2 minutes earlier to allow for clocks), limit (1 to 100, default 20), cursor and include_unconfirmed. Each file has file_id, file_name, type, kind, size, status, usable, source, uploaded_at, expires_at, confirm_before_posting and error, never its address. A read like the others: 60 a minute per account, not counted in the key's request count, Cache-Control: private, no-store. One request reads at most 500 files: follow next_url until it is null
- +Share links in POST /api/v2/media (and the older api-media-upload) are rolling out, switched on account by account, for some accounts first; until they are on for an account, every link is imported exactly as before. Where they are on, a Google Drive, Dropbox, OneDrive or SharePoint link of a file shared with "Anyone with the link" is imported on the paid plans, under the name the provider gives the file. A link that cannot give one file is 400 with code link_not_public, link_is_folder, link_not_found, link_temporarily_blocked, link_not_a_file, link_not_supported or share_links_paused, plus next_step and provider. A Free key's link that needs converting is 403 plan_limit_reached (a link that already downloads the file, such as a drive.usercontent.google.com one, is imported as before), and a plan that cannot be read is 503 uploads_busy with Retry-After 60
- +The AI connector has a 17th tool for signed-in apps, status200_list_media: the same list for an AI app, 10 files a page (at most 50), so a file uploaded in the dashboard is posted by its file_id
- ~The JSON 404 of a path that is no endpoint under /api/v2 lists GET /api/v2/media/files too
GET /api/v2/posts and GET /api/v2/posts/{id} say what we changed to get each TikTok photo post out
- +Every post in GET /api/v2/posts and GET /api/v2/posts/{id}, and every network's entry in a scheduled post's results, has notices: what we changed to get a TikTok photo post out (a photo sent as a smaller or JPEG copy, a title or description shortened), as it was said when TikTok took the post: code, message and, for a photo, photo (for a smaller copy also photos, size and copy_size). [] when there is none and on the other networks, where what we changed (a YouTube title or description we shortened, for example) is still said only in the warnings of the answer that sent or scheduled the post. A TikTok photo sent as a smaller copy (resized_for_tiktok) by a scheduled post, or by a post answered 202 still_publishing, used to be told only in History. Details: https://status200uploads.com/docs/api#tiktok-photos
- +The AI connector's status200_get_post and status200_list_posts carry them as warnings (code and message) on each post and each result, with tell_the_person
- ~A notices field at the top of a POST /api/v2/posts body is not one the API reads (the answer says so: unknown_field), and it is no longer kept with the post either, so a post's notices are only ever what we changed
- ~The copy we store for TikTok in your media library is named after the original file ("IMG_0042 (smaller copy for TikTok).jpg"), no longer after its file id; GET /api/v2/media/files still leaves these copies out
- ~A TikTok post refused with picture_size_check_failed: its error_message no longer says the photo may be too small, and says to save the photo as a JPEG of 1080 × 1350 or 1080 × 1920 pixels and post it again (the code stays in brackets)
The MCP server refuses the API key of a Free account; the REST API is unchanged
- ~The AI connector (the MCP server, https://mcp.status200uploads.com/mcp) is included in every paid plan, from Beginner, whether an app signs in or sends an rl_ API key (Authorization: Bearer or X-API-Key). For the key of a Free account the session still opens and lists its 5 tools, and every tool call answers plan_limit_reached with retry "never" and upgrade_url. The REST API (/api/v2/* and the older api-posts) and its keys are unchanged on every plan
- ~POST /api/v2/media/uploads and POST /api/v2/media/uploads/{file_id}/token can answer 503 upload_links_paused for one account only: uploading from a computer is switched off for that account. The message then says "switched off for this account", retry is "never", there is no Retry-After and asking again does not help: upload the file in the dashboard (Media), or write to info@status200uploads.com. Finishing (/complete) and cancelling an upload are not affected, and neither is posting
Migration Guide
On Free, call the REST API with your API key (https://status200uploads.com/docs/api). To keep using the MCP server, choose a paid plan, from Beginner.
TikTok photos larger than 1080 × 1920 are sent as a smaller copy, or refused before anything is sent
- ~TikTok photo posts: a JPEG larger than TikTok's size (1080 × 1920 pixels, 1920 × 1080 when wide) is sent to TikTok as a smaller JPEG copy inside that size (at least 90% of it), turned the way the photo is shown. TikTok used to accept such a post and fail it a few seconds later with picture_size_check_failed. The warnings in the answer say so (code resized_for_tiktok, with photo, photos, size and copy_size, and the message "Photo too large for TikTok: it is 2160 × 2700 pixels and TikTok takes up to 1080 × 1920, so we sent a smaller copy (1080 × 1350). Your original file is unchanged."), History shows it on the post, and your original file is not changed
- +422 photo_too_large_to_resize, with error.photo: a TikTok photo larger than that size that we cannot make smaller (a WebP; a JPEG over 5 MiB or about 13 MP; a progressive JPEG over 2 MiB or about 12 MP; fewer megapixels with more colour detail or under about 1.8 times the size of its copy; or a JPEG we cannot read: CMYK, 12-bit, arithmetic-coded, lossless, hierarchical or with unusual colour components). Nothing is sent and no post is recorded: send a JPEG of at most 1080 × 1920 pixels
- ~A TikTok post that TikTok refused with picture_size_check_failed has an error_message in plain English that says what to do next, with the code kept in brackets
- +The OpenAPI file describes the warning's photo, photos, from, size and copy_size, and error.photo. Every limit, every message and what to do: https://status200uploads.com/docs/api#tiktok-photos
Upload a file from a computer through a one-time upload link
- +POST https://status200uploads.com/api/v2/media/uploads makes a one-time upload link for a file on a computer: send file_name, size_bytes and sha256 (and shell, powershell or sh, for a ready-made command that runs our uploader). The answer (201) has the file_id, a TUS 1.0.0 address on storage's own host with its signed token (about 2 hours, this file only) and resume_before. Send the file with the command or any TUS client, then POST /api/v2/media/uploads/{file_id}/complete: the file is checked (exact size, type, first bytes, SHA-256) and answered 200 ready (it is in the media library: post it with its file_id in content.mediaID) or 202 verifying (poll GET /api/v2/media?file_id=)
- +POST /api/v2/media/uploads/{file_id}/token gives an unfinished upload a fresh token (only the API key that made the link, at most 12 times, within 24 hours), and DELETE /api/v2/media/uploads/{file_id} cancels it. For an upload, GET /api/v2/media?file_id= answers uploading, verifying, ready or failed, without public_url until it is ready
- +The uploader (Windows PowerShell, sh and Node.js) is published at https://status200uploads.com/uploader/v1/ and a published version never changes. The command checks its SHA-256 before running it, and the API docs list the hashes of every version
- +Limits: videos (MP4, M4V, MOV, WebM) up to 5 GB, images (JPEG, PNG, WebP) up to 20 MB; per account 10 uploads open at once, 20 new links in 10 minutes and 100 a day, and 60 upload-link calls a minute. One daily budget per account and UTC day counts every upload link and the URL imports of connected AI apps: Beginner 2 GB, Apprentice 10 GB, Skilled 25 GB, Expert and Agency 100 GB
- +Paid plans only: Free is answered 403 plan_limit_reached before anything is reserved (upload the file in the dashboard, Media, which every plan can use)
- +New codes: invalid_request, unsupported_media_type, media_too_large, too_many_open_uploads, upload_already_received, upload_closed, renewals_exhausted, account_being_deleted, upload_links_paused, uploads_busy, upload_incomplete, upload_rejected, upload_failed, upload_expired and upload_being_checked, next to plan_limit_reached, import_budget_exhausted, rate_limited, media_not_found and server_error. These answers also carry next_step and, for most refusals, retry ("never" or "after_reset"); the OpenAPI file describes them, and its x-status200-retry table says which may be sent again
- +The x-status200-retry table has a by_operation list, read before by_code for that operation: an answer whose rule is not its code's. Its one entry: POST /api/v2/media/uploads/{file_id}/complete answering 503 upload_links_paused (checking files is paused, and the file that arrived is kept) is wait_and_resend, after Retry-After. A client that reads only by_code gives up there instead; nothing is sent twice either way
- +A budget refusal (429 import_budget_exhausted) carries held_bytes and held_until when unfinished upload links of the account hold part of the day's budget: how much they hold, and when it is free again if nobody continues them
- ~POST /api/v2/posts answers a mediaID that is an unfinished upload with 404 media_not_found and the request that finishes it (POST /api/v2/media/uploads/{file_id}/complete)
URL imports take the six media types the dashboard takes
- ~POST /api/v2/media (and the older api-media-upload) refuses with 415 a URL whose file is not JPEG, PNG, WebP, MP4, MOV, M4V (stored as MP4) or WebM. GIF, HEIC, SVG and other formats were stored before, and a post that used one could then fail
- ~A source that sends no type, or a generic one such as application/octet-stream, is judged by its first bytes: a JPEG, PNG, WebP, MP4, MOV or WebM file is imported as that type, anything else (a web page, for example) is refused with 415
Migration Guide
Convert GIF, HEIC, SVG and other formats to JPEG, PNG, WebP or MP4 before importing them.
Free: 30 posts a month from 18 October
- ~From 18 October 2026 00:00 UTC a Free account gets 429 monthly_limit_reached after 30 successful posts per calendar month (announced before as 60). The codes and fields do not change: error.limit is 30 and error.resets_at is the 1st of the next month
- ~Posts published from 1 October 2026 count toward October's 30, so from 18 October an account with 30 or more used gets monthly_limit_reached until 2026-11-01T00:00:00Z, with error.used above error.limit
- ~Until 18 October, error.limit is 60 for Free accounts created since 18 September 2026, and error.resets_at is the 1st of the next month
- ~On 17 October 2026 a request over the day's 5 posts of an older Free account (5 per profile per day) is answered 429 monthly_limit_reached, with error.resets_at 2026-11-01T00:00:00Z, instead of 202 queued_for_next_day when October's 30 are already used: the queued post would go out on the 18th and be refused
A package for Python and a community node for n8n
- +The Python package status200uploads is on PyPI: pip install status200uploads (Python 3.11 or newer). Its Status200 client publishes, checks, schedules and cancels posts, imports media, reads your profiles, posting options and posts, and can wait until a post is done. By default it sends an Idempotency-Key with every post and media import, and it sends a request again only when the x-status200-retry table of the OpenAPI file allows it. Source: https://github.com/iBoyDroid/status-200-uploads/tree/main/python
- +The n8n community node @status200uploads/n8n-nodes-status200uploads is on npm. On self-hosted n8n: Settings, Community Nodes, Install, then enter @status200uploads/n8n-nodes-status200uploads. It publishes, checks, schedules, reads and cancels posts, lists your profiles and their posting options, and imports media, with an automatic Idempotency-Key. n8n Cloud installs only community nodes that n8n has verified, and this one is not verified yet: on n8n Cloud, use the HTTP Request node as the n8n guide describes
- ~The "Using Python" and n8n guides show the package and the node. The requests example and the HTTP Request node steps stay, for use without them
The whole API as an OpenAPI 3.1 file
- +https://status200uploads.com/openapi.yaml describes every endpoint of both URLs in OpenAPI 3.1: the requests, every answer with its fields, the error codes of each status, the Idempotency-Key and the x-status200-retry table of which answers may be sent again. Import it into Postman or Insomnia, or generate a client from it. It is written from the code and checked against the live answers of both URLs
- *A method /api/v2/posts does not answer (for example PUT) is still 405 method_not_allowed, and now carries a message too: every error of /api/v2/posts and of the reads has error.code and error.message (the media endpoint keeps its {"error": "message"} shape)
- */api/v2/posts reads the word Bearer in any capital letters ("Authorization: bearer rl_..."), as the older URL, /api/v2/media and the reads always did. It answered 401 before
- *The docs name every code the API answers with, among them pinterest_board_required, media_wrong_for_post_type, options_outside_post, no_container, the networks' own refusals (pinterest_rejected and x_rejected with the network's own status), the 401s of Instagram and TikTok that ask for a reconnect (RECONNECT_REQUIRED, REFRESH_TOKEN_EXPIRED, NO_TOKEN, and TOKEN_REFRESH_FAILED when a sign-in could not be renewed just now) and the media endpoint's 405 and import_budget_exhausted. They said queue_failed for a post that could not be queued for the next day: that answer has always been 500 api_error. The queued_for_next_day example shows its upgrade_url and status200, and monthly_limit_reached lists all its fields
- ~The differences of the older URL are listed in full (its api_error refusals of the request, the order of its checks, the answers only /api/v2 gives), and a media source is checked with two requests of 8 seconds each, so up to about 16 seconds
- +A "Using Python" guide. A Python package and an n8n node are coming soon; until then any HTTP library and n8n's HTTP Request node do the same
Send a request again safely with an Idempotency-Key
- +POST /api/v2/posts, POST /api/v2/media and the older api-posts and api-media-upload URLs take an optional Idempotency-Key header: 1 to 255 printable ASCII characters naming this one request. The same key with the same JSON body within 24 hours of the first request gets the first answer back, status and body byte for byte, with Idempotent-Replayed: true, and nothing is posted or imported a second time. A request without the header works exactly as before
- +Remembered: every 2xx (published, processing, still_publishing, scheduled, queued; for media a 200 or 202 with its file_id) and an answer whose outcome could not be confirmed, such as a 5xx platform_error after the post was handed to the network. Not remembered: refusals and failures the network reported with their own code, so a fixed request can be sent again with the same key. A key belongs to the API key that sent it; posts and media are kept apart; the two posts URLs share keys, and so do the two media URLs
- +New answers: 400 idempotency_key_invalid (empty, over 255 characters or not printable ASCII), 409 idempotency_in_progress (the first request with the key is still working; Retry-After 5 and retry_after_seconds), 409 idempotency_outcome_unknown (the first request never finished: check GET /api/v2/posts, then use a new key), 422 idempotency_key_reused (the key was used for a different body; first_used_at) and 503 idempotency_unavailable (the key could not be checked; nothing was sent; Retry-After 5). Nothing is sent with any of them
- +An answer sent again takes no 20-second wait, is logged and counted as a request, and never uses the post allowance. GET, DELETE and dry runs ignore the header. CORS allows the header and exposes Idempotent-Replayed and Retry-After
- ~The n8n guide: with the header, Retry On Fail (Max Tries 5, Wait Between Tries 5000 ms) never posts twice a post whose answer was lost, with a key built from the item and the network such as {{ $json.id }}-tiktok (an id that is never empty), never $now or a random value, and the same body on every try
Read your profiles, posting options and posts, and follow a post with the id every answer now names
- +GET https://status200uploads.com/api/v2/accounts lists your profiles (profile_id, profile_name, handle) and the networks connected to each, with health (ready, reconnect_required or unknown), connected_at and updated_at
- +GET /api/v2/accounts/{profile_id}/options answers what the profile may do on each network, asked of the networks themselves: the TikTok privacy levels post.tiktok.privacyLevel accepts, Pinterest boards for post.pinterest.boardId, Skool groups (and one group's labels, ?skool_group_slug=), the Facebook Page, the Instagram publishing limit and YouTube's choices. One row per network with status ok, not_connected, reconnect_required, unavailable or no_options; always 200 (?platforms= to ask only some)
- +GET /api/v2/posts lists your posts and your scheduled posts that have not produced a post yet (waiting, cancelled, or failed before anything was sent), newest first by at (when a post was sent, when a scheduled post is due). Once a scheduled post has gone out, each network's post is listed instead, with its scheduled_post_id, so nothing is listed twice. limit (1-100, default 20), cursor, status, platform, kind and profile_id filter it; next_cursor and next_url give the next page, and a cursor that is not ours is refused with 400
- +GET /api/v2/posts/{id} reads one post (status, done, link, error, and figures for a successful post: null never means zero; done is true once it will not change by itself, a timeout included once nothing is checking it any more, so a polling loop always ends) or one scheduled post, before and after it goes out (outcome and each network's results)
- +Every POST answer that made a post or a scheduled post, at both URLs and whatever its status, has a new top-level field status200: {post_id, scheduled_post_id, status_url}. status_url is GET /api/v2/posts/{id}. data.post_id and the top-level post_id / scheduled_post_id are unchanged. It is not on a dry run or on a refusal answered before any post was recorded; any answer that left a post in History has it, a failed one included
- +Reads are limited to 60 a minute per account (all its API keys together), and posting options to 10 a minute per account, because each asks the networks themselves: 429 rate_limited with Retry-After, error.retry_after_seconds and error.limit. Reads are not counted in usage_count. Every read answers with Cache-Control: private, no-store
- ~The reads are on status200uploads.com/api/v2 only: GET on the older URL is still 405. CORS on /api/v2/posts now allows GET
- +The AI connector's status200_list_posts also lists posts waiting to go out, cancelled ones and ones that failed before anything was sent, and status200_get_post of a scheduled post shows each network's result once it has gone out
- *The note on a post with no figures in status200_get_post_performance says what is true: figures are collected every 6 hours during the first 7 days after publishing, on the networks that report them. Before, it said an hourly sweep collected them within a few hours
Schedule a post with post.scheduledFor, and cancel it with DELETE
- ~A post.scheduledFor more than 60 seconds ahead now schedules the post: the answer is 202 with "code": "scheduled", scheduled_post_id and scheduled_at (data.status is "scheduled", and there is no "queued" field), and the post is published at that time, usually within a few minutes of it. Before, it was refused with 400 scheduling_not_available_yet. A past time, "now" or a time within the next 60 seconds still publishes at once
- ~A time must say its time zone: ISO 8601 ending in Z or an offset such as +02:00, a Unix time in seconds or milliseconds, or a date with the month as a word as e-mail and HTTP write it ("Wed, 30 Sep 2026 10:00:00 GMT", "... +0200", "... EDT"). A time without a zone ("2026-09-30T10:00:00", or a date alone) is refused with 400 scheduled_for_invalid and error.reason no_time_zone, also when it is in the past (before, a past one published at once). error.reason is unreadable for a value that is not a date and time (a date written with numbers only, such as 01/10/2026, included: it is January in one country and October in another; before, a past one published at once), and too_far_ahead for a time more than 365 days ahead
- +At most 500 posts can wait per account: the next is refused with 409 scheduled_queue_full. When the waiting posts cannot be counted, the answer is 503 schedule_check_unavailable with Retry-After 5, and nothing was scheduled
- +Scheduling works on every plan, Free included, and a scheduled post takes no 20-second wait. The plan gate is checked at once (Skool needs Beginner or higher, X the X add-on: 403 plan_required). The allowance and the X add-on's meter are checked when the post goes out: over a daily allowance it moves to the same time on the next day; on Free, a post over the month's 60 fails and shows on the Scheduled page and in the failure email
- +mediaUrls of a scheduled post, and a Reel's instagram.coverUrl, are fetched when it goes out, so they must still work then: the answer warns with media_url_not_kept, and an address that already answers "not found" is refused with 400 media_not_available (a cover that is gone by the time the Reel goes out leaves the Reel without its cover). An imported mediaID is kept until the post goes out, however far ahead. Skool's group and title are checked at once for a scheduled post, and skool_one_post_per_hour warns when two scheduled Skool posts of your account are less than an hour apart
- +posts_close_together warns when another post of your account on the same network is scheduled within 20 seconds: posts due together go out one after another with no gap, and a network can refuse posts that arrive that close together. Leave at least a minute between them
- +DELETE https://status200uploads.com/api/v2/posts/{id} (and DELETE https://app.status200uploads.com/functions/v1/api-posts/{id}) cancels a post that has not gone out, whoever scheduled it, with the scheduled_post_id of the 202: 200 with data.status "cancelled"; sent again, 200 with already_cancelled: true; 404 post_not_found; 400 bad_request for an id that is not a UUID; 409 not_cancellable once it is being published, published or failed (error.status says which); on /api/v2, 504 cancel_unconfirmed when the answer did not arrive in time (send it again). Each DELETE is logged and counted once in usage_count
- +A dry run with a time answers the outcome "schedule" (reason code scheduled, status 202) and schedules nothing
- +On /api/v2/posts a request with a time whose answer did not arrive in time is answered 202 schedule_unconfirmed: it may have been scheduled, so check the Scheduled page before sending it again
- ~The AI connector reads a time the same way: status200_schedule_post and status200_update_scheduled_post refuse a scheduled_at without a time zone (invalid_request, and the next step asks for the zone) instead of reading it in the server's own time zone, and a date written with numbers only (01/10/2026) as not a date
Migration Guide
Branch on code: "scheduled" means the post was scheduled for your time, "queued_for_next_day" (with "queued": true) that the daily allowance moved it. Never send a request again after a 202 scheduled: it would schedule the post a second time (a DELETE can always be sent again). Send times with a zone (Z or +hh:mm). For a post more than a few hours ahead, import the file (POST /api/v2/media) and send its mediaID rather than mediaUrls.
A post queued for the next day keeps its platform options
- *A pin queued for the next day keeps pinterest.altText and pinterest.dominantColor, as a pin published at once. Before, they were dropped
- ~A pin queued for the next day gets a description only from pinterest.description, as a pin published at once: content.text is its title. Before, content.text was also its description
- *A stored X post keeps x.whoCanReply and x.community (an X post is never queued for the next day: the X add-on has its own allowance, so this matters for scheduled posts)
- *A post queued for the next day keeps instagram.coverUrl, thumbOffset and collaborators (Reels) and locationId (feed images), and tiktok.isAiGenerated on a video, as a post published at once. Before, they were dropped when it went out
Posts we heard nothing back about are checked with the network
- +When a post was sent but no answer came back (the connection broke, or the network was slower than we can wait), we now ask the network ourselves whether the post exists. Facebook: if we find it on the Page, History shows success with its link; if it is not on the Page after a fair wait, History shows it failed and Retry is safe. Instagram: a post we find shows success. The check runs every 10 minutes and gives its answer within about an hour
- ~A Facebook or TikTok photo post sent through the API whose answer was lost is answered 202 still_publishing with its post_id instead of an error, and History shows "Not confirmed" while it is checked (this replaces "timeout, check before retrying")
- ~A post that may already be live and that the network gives us no way to check (X, LinkedIn, Skool, a TikTok post without a TikTok post number, an Instagram post not found yet) cannot be retried: Retry answers 409 outcome_unknown and History says to check the account first, so the same post is never published twice. If it is not there, publish it again as a new post
One engine behind both API URLs
- ~A Facebook post with no media field is a text post on the older URL too: facebook.postType can be left out. On the older URL a media field sent empty ([] or null) without facebook.postType "text" is still refused with 400, so a failed media step never publishes the text alone
- *A Facebook text post without content.text is refused with 400 bad_request "A Facebook text post needs content.text" before anything is sent (the older URL answered 500 and showed a failed post)
- *On the older URL, a profile UUID with spaces around it is found (was 404), a media field sent as text where a list belongs (for example mediaUrls: "https://…") is answered 400 "post.content.<field> must be a list" (was 404 or 500), and an accountId that is not text, or is blank, is answered 400 "post.accountId is required" (was 500, and 404 for a blank one)
- ~TikTok and YouTube connections marked expired are tried and refreshed on the older URL, as /api/v2 already did. When the network no longer accepts the sign-in, the answer is the network's reconnect error (TikTok 401 REFRESH_TOKEN_EXPIRED or TOKEN_REFRESH_FAILED; YouTube "YouTube token expired. Please reconnect your account.") and History shows a failed post; before, the older URL answered 400 "not connected" without trying
- ~Every Facebook and TikTok post appears in History as processing as soon as it is sent, on both URLs. A post whose connection broke off mid-send shows "timeout, check before retrying" instead of not appearing, and a post our gateway lost the answer for (a 504, not a refusal from Facebook) stays processing until the outcome from Facebook is recorded, instead of showing failed
- ~On /api/v2/posts a network slower than the request can wait is answered 202 still_publishing a little earlier. The answer includes post_id whenever the post is already in History, and the request is counted once in usage_count
- ~A connection that breaks after the request was sent is answered 202 still_publishing instead of 502. 502 upstream_unavailable now always means nothing was sent
- *The API page shows your own IP address and client again for /api/v2/posts calls
- ~/api/v2/posts sends YouTube, Facebook and TikTok posts through the same engine as the older URL. Status codes and fields stay the same
- ~The 20-second wait is checked before the connection, as on the older URL: a request that is both too soon and for a network that is not connected gets 429 (was 400)
- ~A refusal because the Free month is used up no longer starts the 20-second wait
- ~Facebook on /api/v2/posts: an empty media list without facebook.postType is refused with 400, as on the older URL (it used to publish a text post)
- *A single .m4v video, or a video URL with #, is sent as a video post
- ~TikTok on /api/v2/posts: when TikTok does not confirm the upload, the answer is 400 (no_publish_id, or TikTok's error), and when our upload of a video stops it is 502 upload_worker_failed (it did not go through: send it again), as on the older URL. When our upload of a photo post stops after TikTok accepted it, the answer is 202 still_publishing (it may be live: do not send it again, History shows the result). Before, /api/v2 answered 2xx and History showed a failed post
- ~TikTok over the daily allowance whose host does not report the file size in time is refused with 400 tiktok_size_unknown instead of being queued
- +dryRun works on /api/v2/posts: send dryRun: true to check a post without sending it. The answer is 200 with dry_run: true and is not counted in usage_count. dryRun: false publishes as normal, and any other value is refused with 400. A check that gets no report in time is answered 503 dry_run_unavailable with Retry-After: send it again
Migration Guide
If your TikTok step treats any 2xx from /api/v2/posts as success, a 400 no_publish_id or 502 upload_worker_failed now means the upload did not go through: send it again. If your Facebook step sends an empty media list for text posts, leave the media fields out instead.
YouTube and TikTok photo posts take a title and a description
- +YouTube videos and TikTok photo posts take a separate title and description, with the same rule in the dashboard, the API (both URLs, and posts queued for the next day) and the AI connector. YouTube: youtube.title (up to 100 characters) and youtube.description (up to 5,000 bytes). TikTok photo posts: the new tiktok.title (up to 90 characters) and tiktok.description (up to 4,000 characters)
- +What you leave out is filled from content.text (the caption). No title: the caption is the title. No description: the caption is the description when you sent a title (on TikTok, a title that is not the same as the caption). With neither, the caption is also the description when the title cannot hold all of it (on YouTube: more than one line, or over 100 characters; on TikTok: over 90 characters). When you send both fields, content.text is not used, and warnings says so with field_not_used
- ~A title or a description that is too long is shortened at the end of a word, never inside an emoji (a text with no space near the limit is cut where it fits), and < and > are removed from YouTube texts. The API refuses nothing for it: the post is sent, and the warnings in the answer say what was changed, with the codes title_shortened, description_shortened, characters_removed, text_partly_used and title_missing. The dashboard asks you to shorten a description that is too long before it posts
- *A TikTok photo post with a caption over 90 characters was refused by TikTok ("The request post info is empty or incorrect"), because the whole caption was sent as the photo title. This happened to 9 customer posts between July and August 2026. Now the title is shortened and the whole caption is the description
- *A YouTube description is measured in bytes, as YouTube measures it (a letter with an accent counts 2, an emoji 4). A description over 5,000 bytes, or with < or >, used to fail at YouTube after the answer said "processing". It is now shortened or cleaned and sent
- ~A TikTok video has one text, its caption, up to 2,200 characters: tiktok.title when sent, otherwise content.text. tiktok.description is not used on a video, and warnings says so
- *A post queued for the next day keeps its YouTube embeddable, license, publicStatsViewable, notifySubscribers and defaultLanguage options, and a scheduled TikTok photo post keeps its description and automatic music. Before, they were dropped
- ~The check that validate_post runs in the AI connector gives the same warning codes as a real post: for a YouTube title, caption_truncated is now title_shortened (Pinterest keeps caption_truncated), youtube_title_missing is now title_missing, and a YouTube text over 100 characters no longer gives caption_over_limit (it was a false warning). A TikTok photo text that is too long now gives title_shortened and description_shortened
Profile names in any capital letters, a clear wait on every 429, YouTube titles, no early posts
- ~accountId finds a profile whatever the capital letters are, on both URLs: "@MyShop" finds "myshop". If two of your profiles differ only in capital letters, send the exact name or the profile UUID
- ~On the older URL (app.status200uploads.com/functions/v1/api-posts) an unknown profile is still answered 404 api_error, now with error.profiles listing your profiles, and the refusal no longer starts the 20-second wait
- +Every 429 carries retry_after_seconds in its body (error.retry_after_seconds on posts, retry_after_seconds on media imports), for tools such as n8n that read only the body. A throttled media import now also has a Retry-After header and "code": "rate_limited", on both URLs. The Retry-After header is sent only when the wait is an hour or less
- ~A post.scheduledFor more than 60 seconds in the future is refused with 400 and nothing is sent. Before, the field was ignored and the post went out at once, earlier than planned. A time in the past or within the next 60 seconds is still ignored. The time can be ISO 8601 with a time zone or a Unix time in seconds or milliseconds
- +A successful answer can carry a warnings array naming each field of the request that was not used and why (for example tiktok.privacyStatus instead of privacyLevel, an option block next to post instead of inside it, X madeWithAi and paidPartnership, LinkedIn carousel)
- +youtube.title sets the YouTube title, also for a post queued for the next day. When a title is sent and youtube.description is empty, content.text becomes the description. Without a title, the title still comes from content.text
- ~TikTok photo posts: PNG and GIF photos are sent to TikTok as JPEG copies so that the post can be published, because TikTok does not accept those formats; JPEG and WebP are sent as they are. HEIC, AVIF, BMP and TIFF photos are refused with 422 photo_format_not_supported before anything is sent. The warnings in the answer say when a photo was converted, and your original file is not changed
- ~The request count of an API key (usage_count, shown on the API page of the dashboard) now counts every request to the older URL, refused ones included, as /api/v2 already does. Before, it counted only posts that were sent or queued
Migration Guide
If you send post.scheduledFor with a future time, remove it to publish at once, or schedule the post in the dashboard. Scheduling through the API is planned.
Videos up to 5 GB on X, LinkedIn, Facebook and Skool, 4 GB on TikTok
- ~Video limits: X, LinkedIn, Facebook and Skool 5 GB (5,368,709,120 bytes), TikTok 4 GB (4,000,000,000 bytes). YouTube stays at 5 GB, Instagram at 300 MB, Threads at 1 GB
- +A single video over 100 MB (X, Skool), 200 MB (LinkedIn) or 1 GB (Facebook) is sent in the background: 202 with data.status "processing", a job_id and a post_id; the result appears in History. Smaller videos answer as before
- +A TikTok video over 1 GB from our storage is fetched by TikTok itself; the answer is 200 "processing" with a publish_id, as for every TikTok post
X add-on, Free monthly allowance, Skool from Beginner
- ~X posts need the X add-on: 403 plan_required with error.required "x_addon"
- +429 x_limit_reached / x_link_limit_reached when the X add-on's 200 posts or 20 link posts of the billing month are used, with resets_at
- ~Free over 60 successful posts per calendar month: 429 monthly_limit_reached with resets_at, not queued
- ~Skool from Beginner
- ~Skilled, Expert and Agency have no allowance; publish attempts capped at 500 per profile per UTC day
Clearer errors, answers within 25 seconds, throttle on every network
- +A mediaID that does not exist on your account (never imported, deleted, or removed after 7 days) is answered at once with 404 media_not_found instead of 409 media_processing
- +POST /api/v2/media refuses a URL that does not return an image or a video with 415, and GET /api/v2/media answers 400 for a file_id that is not a UUID
- ~Every /api/v2/posts answer arrives within about 25 seconds: media still importing is waited for about 12 seconds (then 409 media_processing), and a network slower than that is answered 202 still_publishing (do not resend; the outcome appears in the post history)
- ~The 20-second throttle per network now also applies to TikTok, Facebook and YouTube, and a throttled post carries a Retry-After header
- ~An unknown profile is answered 403 account_not_found on every network, and every error from /api/v2/posts carries error.code
- ~A Facebook post without media no longer needs facebook.postType "text"
- ~X, Pinterest and Skool refusals come back as 4xx or 503 with their own codes (x_rejected, x_unavailable, pinterest_auth_failed, pinterest_sandbox_board, pinterest_rejected, skool_unavailable) instead of 500
- ~YouTube uploads answer 202 with a post_id, and the post history shows the upload's real outcome
- *TikTok photo posts over the size limit are refused with 413 MEDIA_TOO_LARGE before anything is sent
- *Unknown paths under /api/v2 answer a JSON 404 not_found
- !TikTok answers no longer include TikTok's temporary upload_url and upload_token
- +A mediaID whose import failed is answered 422 media_failed with the reason (was 500)
- ~Throttled posts carry error.code rate_limited, and Skool's own limits (one post per hour, 10 per day) answer 429 rate_limited with Retry-After instead of 500
- ~Facebook and LinkedIn answer 401 reconnect_required when they no longer accept the saved sign-in (also on image and video uploads), instead of 500
- ~403 plan_required answers include upgrade_url
Large media imports, size limits up front, Threads
- +Media import accepts videos up to 5 GB. Large imports run in the background, report their progress on GET /api/v2/media?file_id=... and continue automatically after an interruption when the source server supports HTTP Range requests
- +Threads posting (platform "threads"): text only, one image or video, or a carousel of 2 to 20 items
- +accountId on /api/v2/posts accepts "@handle" or the profile name as well as the profile UUID, and /api/v2/posts accepts every network
- ~File size limits per network are checked before anything is sent: a file that is too large for the chosen network is refused with 413 MEDIA_TOO_LARGE
- ~A post whose media is still being imported is answered with 409 media_processing and a Retry-After header instead of a timeout
- ~The daily allowance counts successful posts only: a failed post no longer uses it
- ~A Pinterest post without a board id is refused with 400 before Pinterest is called
- *A single video sent to Instagram without a postType is published as a Reel
Plan limits enforced on the server
- ~Plan limits are enforced on the server for every caller (dashboard, scheduler and API): a daily allowance per profile across all platforms, and X and Skool from the Apprentice plan
- ~A post over the daily allowance is queued for the next day (202 queued_for_next_day) instead of failing
Skool
- +Skool community posts (platform "skool") with text, images and video
X, LinkedIn and Pinterest
- +X (Twitter) posts with text, images and video, reply settings and Communities
- +LinkedIn posts with text, image or video
- +Pinterest image pins with board selection
Instagram, Facebook and YouTube
- +Instagram (feed image, Reel, Story, carousel), Facebook (photo, video, Reel, text) and YouTube video posts
Initial release
- +Create a post endpoint, starting with TikTok
- +Media import from a public URL: the returned file_id can be reused across posts
- +API keys with Bearer token authentication
Ready to get started?
Create an account to get your API key and start automating your social media posts.
Get Started Free