# TikTok Video Uploader Pro - Auto-Post Videos (`dami_studio/tiktok-video-uploader-pro`) Actor

Post a video to TikTok from any public video URL: caption, privacy, confirmation and a link back. The upload fee is charged only on a confirmed post; the bytes a run really moves are billed per megabyte on any outcome, a failure included. Your own proxy is never billed per MB.

- **URL**: https://apify.com/dami\_studio/tiktok-video-uploader-pro.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Social media, Automation, Videos
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $550.00 / 1,000 video uploadeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## TikTok Video Uploader

Posts a video to your own TikTok account from a URL. You give it the video, a caption, and your own exported session cookies, and it drives a headless browser through TikTok's Studio upload page to publish the post. It's meant to be the publish step of a faceless-content or scheduling pipeline.

> **Best-effort browser automation.** There is no official TikTok posting API for this, so the actor automates the real TikTok Studio web UI with your session cookies. TikTok actively detects automation and may show a captcha / "verify it's you" wall, or rate-limit / restrict / ban the account. Run it only on your own account, at your own discretion. When TikTok throws a wall the actor stops with a clear error and a debug screenshot — it does not silently hang.

### How it works

It downloads your video, launches a headless Chromium routed through a residential proxy, loads your exported TikTok cookies into the session, opens the TikTok Studio upload page, sets the file, fills the caption, picks (and verifies) the privacy, clicks Post, and **waits for TikTok's real "uploaded / published" confirmation** before reporting success. There is no shared login — you bring your own session each run.

### Try it with no login (demo mode)

Run it with **no `cookies`** (or set `demoMode: true`) and it returns one clearly-labeled SAMPLE row and uploads nothing. Use this to see the output shape before wiring up your session.

### Input

| Field | Required | Notes |
|---|---|---|
| `videoUrl` | for real uploads | Public direct, non-expired URL to the `.mp4`. You host it (S3, CDN, key-value store). |
| `cookies` | for real uploads | Your TikTok session, exported as Cookie-Editor JSON. Leave blank for a demo run. Stored as a secret. |
| `caption` | no | The caption. Put your `#hashtags` here. |
| `privacy` | no | `public`, `friends`, or `private`. If `private`/`friends` can't be confirmed, the actor refuses to post (so it never publishes Public by accident). |
| `proxyConfiguration` | no | By default the upload session goes out over this actor's own residential exit, and that exit is what the per-megabyte charge pays for (see [Pricing](#pricing)). Set the **country to match the account your cookies came from** — a mismatch triggers captcha walls. Three things take the upload off that exit and switch the per-megabyte charge off with it entirely, on success and on failure alike: your own servers in `proxyUrls` (used exactly as given), selecting a non-residential proxy group, or unticking "Use Apify Proxy". TikTok is far likelier to show a captcha on a datacenter or direct connection, so that saving is a real trade-off, not free. |
| `demoMode` | no | Force a no-upload sample run even with cookies. |

### How to export your TikTok cookies

1. Log into **tiktok.com** in Chrome/Edge as the account you want to post from.
2. Install the **Cookie-Editor** extension and open it on a tiktok.com tab.
3. Click **Export → Export as JSON** (this copies the whole cookie set to your clipboard).
4. Paste that JSON into the `cookies` field.

The export must include `sessionid` (and ideally `sid_tt`, `tt_csrf_token`, `msToken`) for `.tiktok.com` — `sessionid` alone is often not enough. **Export from the same country your proxy uses.** Cookies expire; re-export when you see a "not logged in" error.

### Output

One dataset record. On a confirmed upload: `{ ok: true, platform, videoUrl, videoId, caption, privacy, privacyApplied, postedAt, runId }`, where `videoUrl`/`videoId` point at the published post (resolved from the content manager; may be `null` if TikTok didn't surface it). On failure: `{ ok: false, error }`, plus a `bytesMoved` block giving the exact byte counts this run moved and whether they crossed the metered connection, a `chargeStatus`, and a debug screenshot + page HTML in the run's key-value store. Every run also writes `CHARGE_SUMMARY.json` to that store with the settled billing figures. Demo runs carry `_demo: true` and a `_notice`.

### Example

```json
{
  "videoUrl": "https://cdn.example.com/clip.mp4",
  "caption": "new drop today #fyp",
  "privacy": "public",
  "cookies": "[{\"name\":\"sessionid\",\"value\":\"...\",\"domain\":\".tiktok.com\",\"path\":\"/\"}]"
}
```

### Pricing

Three charges, no volume tiers and no plan gates:

| Charge | Amount | When it applies |
|---|---|---|
| Run start | $0.015 per event | Every run. The platform bills one start event per gigabyte of run memory, and this actor is fixed at 2 GB, so a run start costs **$0.03**. |
| `tiktok-upload` | $0.55 | Once per run, and **only after TikTok returns its real published confirmation**. A run that publishes nothing never pays this. |
| `video-megabyte` | $0.02 per MB | The megabytes the run actually moved over **this actor's own metered residential connection**, rounded up. Charged on every outcome, including runs that fail. Never charged at all on your own proxy, a non-residential group, or no proxy. |

#### Bandwidth is charged even when the post does not land

This is the one thing to read before you run it, because it changed on **25 August 2026**.

The upload fee is still a success fee: if nothing is published, you are not billed for publishing. The per-megabyte line is different, and it is not really a fee — it is the cost of moving your video. TikTok checkpoints datacenter addresses on the upload flow, so this actor uploads through a residential connection it rents by the gigabyte. Those gigabytes are bought the moment they are used and cannot be handed back. A run that pulls your 30 MB file in and pushes it at TikTok has spent that bandwidth whether TikTok then confirms the post, throws a captcha, or changes its composer.

So the per-megabyte charge bills the megabytes the run really moved, measured as they move, on whatever outcome it reaches:

- your cookies turn out to be dead, or TikTok shows a "verify it's you" wall;
- TikTok refuses the post, or its composer changed shape underneath the automation;
- Post was clicked and TikTok never confirmed it;
- the run hit its own time limit (an abort is different — see below);
- the download broke part-way, or the file turned out to be over the size ceiling once the transfer was already running.

Two things it does **not** do. It never bills bytes that did not move — a run refused before the first byte costs nothing but the platform start fee, and a byte is counted where it crosses the wire, so an origin that compresses your file is billed for what it actually sent, not for the file's unpacked size. And it never bills more than the same run would have cost had it published: the ceiling is your file's own megabytes plus what the $0.55 upload fee would have covered, so a failed run always comes out cheaper than the successful version of itself — retries of a broken download included.

Every run writes the numbers down. The result row carries a `bytesMoved` block with the exact byte counts, and `CHARGE_SUMMARY.json` in the run's key-value store carries the settled figures taken from the platform's own answer — including how many megabytes were charged and how many the actor absorbed, if your `maxTotalChargeUsd` stopped the charge short.

Two honest limits, both measured rather than assumed. **An aborted run is not charged for its bandwidth.** The actor does listen for the abort, but when this was tested on 25 August 2026 the platform ended the run 212 milliseconds after announcing it — not the 30 seconds it advertises — which is not enough time to write a row or raise a charge. The same goes for a run the platform kills outright on its own timeout, and for a server migration. And if the actor cannot write you a result row at all, it charges nothing: no receipt, no bill.

#### What a run costs

| Run | Over this actor's residential exit | Over your own proxy, a non-residential group, or no proxy |
|---|---|---|
| 30 MB video, published | $0.03 + $0.55 + $0.60 = **$1.18** | $0.03 + $0.55 = **$0.58** |
| 300 MB video, published | $0.03 + $0.55 + $6.00 = **$6.58** | $0.03 + $0.55 = **$0.58** |
| 30 MB video, dead cookies — file downloaded, session opened | $0.03 + about $0.74 of bandwidth | **$0.03** |
| Refused before anything moved (budget, bad input, bad cookie format) | **$0.03** | **$0.03** |
| Demo / sample run | **$0.03** | **$0.03** |

That failure figure is an example, not a fixed price: it is 30 MB of video plus roughly 7 MB of TikTok Studio. Your own run's exact number is in its row, split into the two legs. Worth knowing which is which: your file is pulled straight from `videoUrl`, not through the residential connection, so on a run that fails *before* the file reaches TikTok those megabytes are billed as bytes the run moved rather than as residential bandwidth. Only the TikTok Studio traffic in that example crossed the metered connection.

#### When the per-megabyte line is zero

`video-megabyte` pays for a metered connection, not for the file, so it is not charged at all — success or failure — when the bytes did not go over that connection. That is the case whenever you:

- put your own servers in `proxyConfiguration.proxyUrls` — you already paid for that bandwidth once;
- select a proxy group other than residential, so the run goes out over rotating datacenter addresses; or
- untick "Use Apify Proxy" and connect directly.

In all three the run pays the $0.03 start fee, plus the $0.55 upload fee if it publishes, and nothing more — whatever the video weighs and however the run ends. The run log states which exit was used and says explicitly when no per-megabyte charge was made. The trade-off is real: TikTok checkpoints datacenter and bare container addresses on the upload flow, so those runs hit captcha walls far more often.

#### Why the per-megabyte charge exists

An upload session needs a residential exit — TikTok walls the cheap ones — and residential bandwidth is billed by the gigabyte. Part of that traffic is fixed: booting Studio and driving the composer moves tens of megabytes whatever you post, and on a confirmed post the flat $0.55 covers it. The video's own bytes are the part that scales, and a flat fee alone stops covering them right around the median clip size. Billing those megabytes is what lets a large file go through and simply cost more, instead of being refused for being large.

#### Size limits and your own budget

A video above **500 MB** is refused, on any connection. On the residential exit only, if you set `maxTotalChargeUsd` on the run, the actor checks *before* downloading that the budget still covers the megabytes it is about to bill; if it does not, the run stops with `CHARGE_BUDGET_EXCEEDED` before moving a byte, and charges nothing. That check does not exist on a connection of your own, because nothing there is billed by the megabyte.

`maxTotalChargeUsd` is honoured on the failure path too: the actor asks the platform how much of your budget is still available and charges no more than that, and `CHARGE_SUMMARY.json` says plainly when your cap stopped the charge short and how many megabytes the actor absorbed as a result.

### Notes

This automates your logged-in TikTok session, which TikTok's terms discourage — see the warning at the top. Because it drives TikTok's actual web UI, a layout change on their side can break a selector and need an update; when that happens the actor fails with an actionable message and saves a screenshot rather than reporting a false success.

# Actor input Schema

## `videoUrl` (type: `string`):

Public direct URL to the video file (.mp4) to upload. Must be a non-expired direct link (S3/CDN/key-value store). Required for a real upload; leave it blank for a keyless demo run.

## `caption` (type: `string`):

The TikTok caption (include #hashtags here).

## `privacy` (type: `string`):

Who can watch the video.

## `sessionid` (type: `string`):

Your TikTok "sessionid" cookie. In your browser: DevTools > Application > Cookies > https://www.tiktok.com, find the row named sessionid and copy its Value column. Paste just the value.

## `sid_tt` (type: `string`):

Your TikTok "sid\_tt" cookie, from the same Cookies list. Paste just the value.

## `cookies` (type: `string`):

The most reliable way to authenticate: paste a Cookie-Editor JSON export of the entire tiktok.com cookie list, or a raw "Cookie:" header. TikTok’s upload page often needs more than the two named cookies above, and a whole-list export cannot miss one. Fill in either this field or the two above — if both are given, this one wins.

## `proxyConfiguration` (type: `object`):

Proxy for the upload session. TikTok checkpoints datacenter addresses on login and upload, so the default is a residential exit — and that residential bandwidth is what the per-megabyte charge pays for. Set the COUNTRY to match the account your cookies come from; a mismatch triggers captcha walls. IMPORTANT: on this residential exit the per-megabyte charge bills the megabytes a run really moved, and it does so on every outcome — including a run that fails, because the bandwidth was already bought. Two choices here remove that charge entirely, on success and on failure alike, because the video no longer crosses a connection this Actor pays for: selecting any NON-residential group, or putting your own servers in "Proxy URLs" (used exactly as given). Both trade a higher captcha risk for a lower bill. RESIDENTIAL is the only metered group this Actor will ever open.

## `demoMode` (type: `boolean`):

Force a labeled SAMPLE run that uploads nothing, moves no video bytes and is charged nothing beyond the platform's own run-start fee, even if cookies are provided. A keyless run (no cookies) is automatically demo mode.

## Actor input object example

```json
{
  "privacy": "public",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "demoMode": false
}
```

# Actor output Schema

## `results` (type: `string`):

Result rows / metadata are stored in the default dataset (one row per item). A run that did not publish writes one labelled diagnostic row carrying the error, a "bytesMoved" block with the exact bytes the run moved, and a "chargeStatus".

## `files` (type: `string`):

Generated media/files and debug artefacts are stored in the default key-value store, alongside "CHARGE\_SUMMARY.json" — the settled billing figures for the run, taken from the platform's own answer to each charge call.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/tiktok-video-uploader-pro").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/tiktok-video-uploader-pro").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call dami_studio/tiktok-video-uploader-pro --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/tiktok-video-uploader-pro"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/i4vYTKFipy7khXR9M/builds/OlgN9AryGI7vE5f8A/openapi.json
