# YouTube Uploader Pro - Videos (`dami_studio/youtube-video-uploader-pro`) Actor

Publish a long-form video to your own YouTube channel from a URL: title, description, tags, category, playlist, thumbnail, visibility and scheduling. You bring your own session cookies. It reads the file first where the host allows it, and refuses uploads YouTube would silently ruin.

- **URL**: https://apify.com/dami\_studio/youtube-video-uploader-pro.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Social media, For creators, Automation
- **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

## YouTube Uploader Pro — Videos

Publishes a long-form video to your own YouTube channel from a URL.

You give it a direct link to an mp4 and a title. It opens YouTube Studio in a real browser, signs in with your cookies, fills in the wizard, waits for the bytes to finish moving, and reports back only what YouTube actually confirmed. Title, description, tags, category, playlist, custom thumbnail, made-for-kids, visibility, and scheduled publishing with a real timezone are all set for you.

This is the long-form edition. If your clip is vertical and under three minutes, use the Shorts edition instead — YouTube would classify it as a Short here anyway, and this Actor refuses that case rather than let it happen (see below).

### What you need

- A public direct link to the video file. It has to serve the file itself, not a page with a player on it. S3, a CDN, or an Apify key-value store record all work.
- A title.
- Your YouTube cookies, pasted once. Instructions below.

### Costs

You are billed for the run start, for every megabyte of video the run transfers, and for the publish once YouTube confirms it. Megabytes are counted on every attempt, including a failed upload and a transfer that had to restart, so a run can bill more megabytes than the file holds. The prices are listed on this Actor's store page.

### Read this first

This drives the YouTube Studio web interface with your session cookies. That goes against YouTube's Terms of Service, and if YouTube objects it is your channel that pays for it, not this Actor. Use it on channels you own and are willing to risk.

The official Data API is not a way around that. Videos uploaded through `videos.insert` from an unverified API project created after 28 July 2020 are locked to private forever, and YouTube's own help pages say there is no appeal — you delete and re-upload. That is why there is no API mode here.

### Getting your cookies in

The Actor signs in as you, using the cookies from a browser where you are already signed in. You copy them out once and paste them into the input.

1. **Install Cookie-Editor in Chrome.** [Chrome Web Store](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm).

   Chrome is the browser these steps were written against and the only one they were tested in. Step 2 turns on a permission from `chrome://extensions`, a page Firefox does not have. Cookie-Editor has a Firefox build and its JSON export is the same shape, so it may well work — but nobody here has run it, so treat a Firefox export as untested.

2. **Give Cookie-Editor access to every site.** Open `chrome://extensions`, find Cookie-Editor, click Details, and set site access to "On all sites". Do this before you export anything.

   Without it, SID, HSID and APISID are missing from the export. The export looks complete either way, which is what makes this the step people lose an hour to.

3. **Sign in to YouTube in that browser,** on the account that owns the channel you want to upload to. If the account has several channels, switch to the right one now.

4. **Go to `https://www.youtube.com/robots.txt`, open Cookie-Editor and choose Export → JSON.** That copies the cookies to your clipboard. Nothing visible happens, which is normal.

   robots.txt rather than the YouTube home page because it is a plain text file with nothing loading behind it. A normal YouTube page keeps making requests while you sit on it, and one of those can re-mint a cookie between your export and your paste.

5. **Paste it into the box labelled "YouTube session cookies — paste both exports, then leave empty".**

6. **Now go to myaccount.google.com and do Export → JSON again.** Paste that second export into the same box, straight after the first one. Leave the first one where it is. Two blocks of JSON in one field is what the Actor expects, and it sorts them out itself.

   The reason you do it twice: the extension can only ever copy the cookies for the site you are currently on, and the upload passes through both youtube.com and accounts.google.com. This is measured, not theoretical. An export from youtube.com on its own gets a sign-in page.

7. **Start the run within a few minutes.** One pair in the session, `__Secure-1PSIDTS` and `__Secure-3PSIDTS`, is re-minted about every ten minutes. An export that has sat in a text file for an hour carries a pair Google replaced several times over.

8. **After the first run that works, empty the cookie field and leave it empty.** The Actor keeps the session and refreshes it every time it runs. Paste again only when you want to upload to a different account, or when a run tells you the session stopped working.

### When Google says "Verify it's you"

Sometimes Google shows a verification screen instead of Studio. The run stops there with a `SESSION_CHECKPOINT` code and uploads nothing. There is no way for the Actor to answer that screen for you.

Clear it in your own browser: open YouTube on that account, do whatever Google asks for — a code, a tap on your phone — and get back to a normal signed-in page. Then export your cookies again and start a new run. Re-exporting on its own does not help if you have not cleared the challenge first; the session stays flagged.

Matching the proxy country to where you exported your cookies is worth doing, but do not count on it: a run through an exit in the account's own country met this same challenge. What actually sets it off is not something this Actor can see.

### Where your session is kept

Once a run signs in and reads back which channel it landed on, it saves the refreshed cookie jar so you do not have to paste again. Both halves are needed. A run that signs in but cannot get Studio to name the channel saves nothing, leaves whatever is already stored untouched, and says so in the log — cookies a run cannot attribute are how a session for the wrong account becomes permanent.

The jar lives in a key-value store called `youtube-uploader-sessions` **in your own Apify account** — the account that runs the Actor. Nothing is sent to the developer, and no cookie value ever goes into the run's own output store or into a dataset row. That record is a working signed-in session for your Google account, so treat it the way you treat the cookie export itself.

It does not expire. Three things clear it:

- Tick **Forget the saved session** (`resetProfile`) and paste a fresh cookie export in the same run. The old record is deleted before anything reads it, and rebuilt from what you pasted. Ticking it without pasting cookies does nothing at all, on purpose: a run with no session to sign in with should not be able to destroy your saved one.
- Delete it yourself in Apify Console → Storage → Key-value stores → `youtube-uploader-sessions`. The record is named `session-<channelId>`, or `session-default` when you have not set a `channelId`.
- A run that lands on a channel other than the one it asked for clears the records it read or wrote on that run, so a session that publishes to the wrong place cannot settle in. Records it neither read nor wrote are left alone.

To switch accounts you only need to paste. Anything you paste wins outright, the saved jar is not consulted on that run, and the stored copy is replaced at the end of it.

Deleting the record does not sign the Google account out. For that, use Google's own Security → Your devices → Sign out, which kills every copy of that session including any you exported.

### Try it without signing in

Run it with no cookies on an account that has no saved session yet, or tick `demoMode`, and you get one clearly labelled sample row. It uploads nothing and moves no video bytes. The run start is still billed. Use it to see the output shape before you wire up a session.

Once a session is saved, an empty cookie field means a real upload — that is the point of step 8. Two things reach the sample row after that: `demoMode`, or **Forget the saved session** ticked with the cookie field still empty.

### What it does, in order

1. Checks your input with no network at all.
2. Checks the cookie jar and fails fast on one that has already expired.
3. Opens a headless Chromium through a datacenter exit and injects your session.
4. Proves the session on Studio **before** downloading anything.
5. Reads up to two megabytes of your video over range requests and applies every rule below, before the full transfer. One kind of file cannot be read that way, and for that one the checks move to step 6 instead.
6. Downloads the file from its origin and re-checks the real bytes.
7. Drives the wizard, reading back every control it touches — the title box, the audience radio, the visibility radio.
8. Waits for the byte upload to finish, publishes, and confirms.

### What it refuses

The run finishes as succeeded and writes one row carrying the code below, a plain-English `error` saying what to change, and a `hint`. Nothing is published.

| Code | What happened |
|---|---|
| `WOULD_BECOME_SHORT` | The frame is square or taller **and** the clip is three minutes or under. YouTube files that as a Short, server-side, with no flag to turn it off and no way to undo it after publishing except deleting and re-uploading. You would have paid for a long-form upload and got a Short. Use a wider aspect ratio such as 16:9, or the Shorts edition of this Actor. |
| `UNVERIFIED_CHANNEL_15MIN` | Over 15 minutes with **Channel is phone-verified** left unticked. An unverified channel is capped at 15 minutes, and YouTube enforces it the expensive way: it accepts the whole file and rejects it afterwards with `rejectionReason: "length"`. Verify at youtube.com/verify, then tick the box. One phone number verifies two channels a year. |
| `VIDEO_TOO_LONG` | Over 12 hours. YouTube's hard ceiling is 256 GB or 12 hours, whichever comes first. |
| `NO_VIDEO_TRACK` | Audio-only file. An MP3 or WAV cannot become a YouTube video. Render the audio onto a video track first. |
| `VIDEO_TOO_SMALL` | Under 2 KB, which YouTube answers with "Rejected (file too small)". Check that your URL points at the video and not at an error page — an expired signed URL returns one with HTTP 200. |
| `UNSUPPORTED_CONTAINER` | The file's opening bytes name a container this Actor does not upload. It takes MP4/MOV and Matroska/WebM; AVI, FLV, Ogg, WMV and the MPEG streams are refused on the first megabyte, before the rest of the file moves. Re-encode to MP4 (H.264 video, AAC audio). |
| `OVERSIZED_VIDEO` | Three things fire this and the message says which. (1) The file is above the 4000 MB ceiling — compress it or split it. (2) It fits the ceiling, but this run's **Maximum total charge (USD)** (`maxTotalChargeUsd`) does not leave room for it — raise the budget or send a smaller file; nothing is wrong with the video. (3) The run could not establish what it is allowed to charge for those megabytes, so it declined to move them; the file and your settings are both fine, so send something smaller or try again later. The declared `content-length` is checked first, so an oversized file is normally refused without being downloaded. |
| `WRONG_CHANNEL` | The session landed on a channel other than the `channelId` you asked for. Studio redirects to the account's default channel without saying so when the account does not manage the one you named. There is no error to catch, so the run stops rather than publishing to the wrong place. |
| `VISIBILITY_UNCONFIRMED` | The visibility setting could not be read back off the radio, so the run refused to press Publish. Clicking a radio button and having it selected are two different things, and the gap between them is a video that should have been private going public. The upload is saved as a draft in your Studio — open it and set visibility by hand, or run it again. |
| `BAD_INPUT` | Something in the input stops the run before it does any work. A missing or non-https `videoUrl`, a missing `title`, a `channelId` that is not a `UC…` id, cookies that could not be read, a **Maximum total charge (USD)** too low to cover the publish fee, or a `publishAt` that is in the past or carries no UTC offset. The offset is required because without one the schedule lands in whatever timezone Studio is showing, and YouTube renders published dates in Pacific Time on the watch page, so an ambiguous time reads back as the wrong day. |

Warnings that do not stop the run: a title or description over the 100/5000-character limits (cut, and the log says by how much), more than 60 hashtags across the two (past that YouTube ignores all of them), and an MP4 whose `moov` atom sits at the end of the file rather than the front — a common cause of "Processing abandoned". Fix that one with `ffmpeg -i in.mp4 -c copy -movflags +faststart out.mp4`.

#### Session, run and Studio codes

These are the codes that are not about your file. They end the same way as the ones above: the run finishes as succeeded, writes one row, and publishes nothing.

| Code | What happened |
|---|---|
| `SESSION_INCOMPLETE` | The pasted cookies do not add up to a Studio session. Check that both exports are in the box, and that Cookie-Editor had access to all sites before you took the first one. |
| `SESSION_INVALID` | A session cookie could not be used — either its own expiry had already passed, or none of them survived being loaded into the browser. Re-export both sets and start the run within a few minutes. |
| `SESSION_EXPIRED` | Google served its sign-in page instead of Studio. Re-export both sets, including `__Secure-1PSIDTS` and `__Secure-3PSIDTS`, which rotate about every ten minutes. Do not keep browsing YouTube on that account afterwards — an open tab rotates the cookies you just exported. |
| `SESSION_CHECKPOINT` | Google served a "verify it's you" challenge instead of Studio. There is a section on this above. |
| `BROWSER_GATE` | The run reached Google's sign-in form and was blocked there with "this browser or app may not be secure". Reaching a sign-in form at all means the session was not accepted as signed in, so the thing to re-export is the session. |
| `VIDEO_UNAVAILABLE` | The file could not be fetched. A bad status, a timeout, a content type that is not a video, zero bytes, or an HTML page where the video should be. An expired signed URL returns HTTP 200 with an error page, which is why the file's magic bytes are checked and not its content type. |
| `DAILY_LIMIT` | The channel's 24-hour upload cap. There is a section on this below. |
| `UPLOAD_REJECTED` | YouTube refused the file after it arrived. The row quotes Studio's own sentence, which does not name what YouTube objected to. |
| `UPLOAD_UNCONFIRMED` | Publish was clicked and Studio never confirmed it. The video may still have published — open Studio and look before you retry, because a blind retry can double-post. |
| `LAYOUT_CHANGED` | A step in Studio answered no selector this Actor knows. There is a section on this below. |
| `NETWORK` | A network or proxy error stopped the run reaching YouTube. Retry, or change the proxy country. |
| `TIMED_OUT` | The run hit its time limit waiting on YouTube and stopped itself rather than being killed. Check the channel before retrying: if Publish had already been clicked, YouTube may still finish the upload. |
| `INTERNAL` | An unexpected error inside the Actor. The run id and the saved screenshot are what get it fixed. |

#### How much of your file moves before a refusal

Every megabyte the run transfers is billed, including on a run that ends in one of the codes above, so it is worth knowing which refusals land before the file moves.

Most of the checks above run on a `Range` request for the first megabyte of your URL, before the rest of the file has moved. If that megabyte does not carry what the checks need, one more is read from the end. The probe reads 2 MB at most and never reads past it.

A container that is not MP4 or MOV is named from that first megabyte. AVI, FLV, Ogg, WMV and the MPEG streams are refused there with `UNSUPPORTED_CONTAINER`, and the rest of the file never moves — each carries a fixed signature in its opening bytes, and no later byte can turn one of them into an MP4. That holds even when your host ignores the `Range` header and starts sending the whole file: the probe stops reading at a megabyte either way. Matroska and WebM are named the same way and then uploaded, because YouTube accepts them.

What a Matroska or WebM costs you is two checks. Neither the duration nor the frame size can be read out of that container, so `WOULD_BECOME_SHORT` and `UNVERIFIED_CHANNEL_15MIN` cannot run on it, and the log names both. A square-or-taller WebM of three minutes or less will publish as a Short you paid long-form pricing for, and one over 15 minutes on an unverified channel will be rejected by YouTube after every byte has moved. Send those two shapes as MP4.

One kind of file falls through the probe entirely: a valid MP4 or MOV whose `moov` atom sits at the end, on a host that ignores `Range` and sends the whole file from byte zero. The first megabyte holds no `moov`, and the request for the last megabyte comes back as the first one again. So that file is downloaded in full, read off disk, and judged then, and a `WOULD_BECOME_SHORT` or `UNVERIFIED_CHANNEL_15MIN` refusal can arrive after the whole file has crossed.

Serve a faststart MP4 from a host that answers a ranged request with HTTP 206 and the decision stays on the first megabyte:

```
ffmpeg -i in.mp4 -c copy -movflags +faststart out.mp4
```

The transfer is bounded whatever the file is: a `content-length` declared above the 4000 MB ceiling is refused before a byte moves, bytes are counted as they arrive so a server that declares no length is still cut off part-way once it passes the ceiling, and retries after a broken transfer count against one running total for the whole run instead of starting again from zero.

### After publishing: low resolution is normal

The video id and the watch URL exist right away, but YouTube processes resolutions in stages. The watch page may show only SD for a while, and 1080p or 4K fill in later. YouTube's own guidance is that a 60-minute 4K30 video can take up to four hours.

That is normal, not a failure. This Actor does not wait for it. Every successful row repeats it in the `note` field. The same goes for the first few minutes, when the watch page can read "unavailable" until the SD version lands.

### Input

| Field | Required | Notes |
|---|---|---|
| `videoUrl` | for real uploads | Public https direct link to the file. You host it. |
| cookies | first run only | Both exports, in the one field. Empty after that, once a session is saved. See above. |
| `title` | for real uploads | Cut to 100 characters, loudly. A blank one is refused with `BAD_INPUT`: Studio pre-fills the title box with your filename, so a blank title would publish a video called `video.mp4`. |
| `description` | no | Cut to 5000 characters, loudly. |
| `tags` | no | Entered in the advanced pane; the chips are read back afterwards. |
| `visibility` | no | `public`, `unlisted` or `private`. Defaults to private. |
| `publishAt` | no | RFC 3339 with an explicit offset, e.g. `2026-09-01T14:00:00-07:00`. A scheduled video is private until its moment, so visibility is set to private for you and the log says what it becomes and when. |
| `categoryId` | no | People & Blogs (default), Entertainment, Comedy, Education. |
| `playlistId` | no | Best-effort. Studio's playlist picker is a search box, so a name that cannot be matched is reported rather than guessed at. |
| `channelId` | no | The `UC…` id, not the `@handle` and not the Brand-account id. |
| `thumbnailUrl` | no | Best-effort, under YouTube's own 2 MB limit. Custom thumbnails need a phone-verified channel; on an unverified one Studio takes the click and does nothing. The row reports whether it landed. |
| `madeForKids` | no | The COPPA audience declaration. Binding on you, not on this Actor. |
| `channelIsPhoneVerified` | no | Opens up the over-15-minute path. |
| `resetProfile` | no | Deletes the saved cookie jar before anything reads it. Only does anything when you paste a fresh export in the same run. |
| `proxyConfiguration` | no | The group you pick is ignored — this Actor picks its own datacenter lane in code. What does take effect is the country (match where you signed in) and your own servers in `proxyUrls`. |
| `demoMode` | no | Force a no-upload sample run even with cookies. |

`maxTotalChargeUsd` sits in the run options rather than the input form. A cap set below what the run needs stops it rather than shrinking the bill: too low for the publish fee ends the run with `BAD_INPUT` before a browser opens, and too low for your file's megabytes ends it with `OVERSIZED_VIDEO` after the probe and before the transfer. Long-form files make the second one easy to walk into.

### Example input

```json
{
  "videoUrl": "https://cdn.example.com/episode-14.mp4",
  "title": "Episode 14 - the long version",
  "description": "Chapters in the comments.\n\n#podcast",
  "tags": ["podcast", "interview", "long form"],
  "categoryId": "22",
  "visibility": "private",
  "publishAt": "2026-09-01T14:00:00-07:00",
  "channelIsPhoneVerified": true,
  "channelId": "UCXXXXXXXXXXXXXXXXXXXXXX"
}
```

### Output

One dataset row per run, always. A confirmed publish looks like this:

```json
{
  "ok": true,
  "billable": true,
  "kind": "video",
  "published": true,
  "confirmed": true,
  "videoId": "dQw4w9WgXcQ",
  "watchUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "studioUrl": "https://studio.youtube.com/video/dQw4w9WgXcQ/edit",
  "title": "Episode 14 - the long version",
  "tags": ["podcast", "interview", "long form"],
  "tagsApplied": true,
  "categoryId": "22",
  "visibility": "private",
  "visibilityConfirmed": true,
  "scheduled": true,
  "publishAt": "2026-09-01T14:00:00-07:00",
  "durationSec": 1847.2,
  "width": 1920,
  "height": 1080,
  "videoBytes": 519045120,
  "sessionRefreshed": true,
  "warnings": []
}
```

The row also carries `platform`, `playlistId`, `madeForKids`, `thumbnailApplied`, `alteredContentSet`, `draftSaved`, `checksState`, `sourceUrl` with the query string masked, `sessionRefreshNote`, `egress`, `note`, `publishedAt` and `runId`.

`scheduled` and `publishAt` are `false` and `null` unless you asked for a schedule. `warnings` collects anything best-effort that did not land — a thumbnail Studio ignored, a playlist name it could not match, tags that did not read back.

If Studio confirms the publish but a later step stalls, the row is written from the confirmation rather than abandoned: the video is live, and the fields that come from those later steps can be `null` or `false`, with a `warnings` entry saying so.

Anything else gives you `ok: false`, a stable `errorCode`, a plain-English `error` telling you what to change, and a `hint`. Every code this Actor can raise is in the two tables under "What it refuses"; `SESSION_CHECKPOINT`, `LAYOUT_CHANGED` and `DAILY_LIMIT` also have sections of their own. The run finishes as succeeded either way, because crashing would tell you less.

The result row is written before the charges are made, so a failed row cannot say what the run cost. It carries `charged: null` for that reason, and `uploadFeeCharged: false`, which is the part it does know. What was actually charged is written afterwards to the run's key-value store under `CHARGE_RESULT`.

#### What lands in the run's key-value store

When a step fails, a screenshot and a copy of the page HTML go in so you can see what the browser saw. They are named after the step that stuck, or `yt-error` when nothing more specific was reached.

The HTML is scrubbed before it is stored. Script bodies, cookie names and token-shaped strings are replaced with placeholders, because a signed-in Studio page carries live auth material and this store is a customer-facing output. Read it for page structure and visible text, and do not conclude anything from what is missing. A page that could not be scrubbed is not stored at all, and the log says so.

The screenshot is not scrubbed and cannot be. It is a picture of a signed-in Studio page, so it shows the account name and the avatar. That is deliberate — the rendered page is the half that shows you which step stuck — and it is your own account in your own store. Treat it the way you would treat a screenshot of your own browser, and think about that before you hand anyone the store or its id.

No cookie value is ever written here. The session lives in `youtube-uploader-sessions`, which is a different store.

### When it breaks

Because it drives the real Studio interface, a YouTube layout change can invalidate a selector. When that happens the run stops with `LAYOUT_CHANGED` naming the step and saves a screenshot plus the scrubbed HTML. It never reports a success it did not see. If it died mid-wizard it clicks **Save and close** first, so YouTube keeps one recoverable draft instead of an abandoned partial upload.

One more code worth knowing: `DAILY_LIMIT` is a per-channel 24-hour cap that YouTube has never published a number for, so the run cannot tell you how many uploads you have left or when the window reopens. Retrying inside the window has not been observed to clear it. Wait a day. Phone-verifying the channel is the documented lever on the cap.

### Notes

- Run one upload at a time per account. Two runs on the same session look like a hijack to Google.
- The video is pulled from your URL directly, not through the proxy. What does go through it is the browser's own traffic: the Studio session, including the push up to YouTube.
- Studio's "altered or synthetic content" question is answered No. If your videos need that disclosure, set it by hand in Studio afterwards.

# Actor input Schema

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

Public https direct URL to the long-form video file (.mp4/.mov) to upload. Must be a non-expired direct link (S3/CDN/key-value store) that returns the file itself, not a landing page. Required for a real upload. To see the sample row instead, run it with the cookie fields blank on an account that has no saved session yet, or tick "demoMode".

## `title` (type: `string`):

The video title, capped at YouTube's 100 characters (a longer one is cut and the run logs exactly how many characters were dropped, never silently). Required for a real upload: YouTube pre-fills the title box with the video's filename, so a blank title publishes a video called "video.mp4".

## `description` (type: `string`):

The video description. YouTube's limit is 5000 characters; anything longer is cut to 5000 and logged. Over 60 hashtags across the title and description makes YouTube ignore ALL of them on the video, so the run warns above that.

## `tags` (type: `array`):

Video tags. They are entered into Studio's advanced pane and the chips are read back afterwards, so the result row can say whether they actually landed. YouTube caps the combined tag string at 500 characters.

## `visibility` (type: `string`):

Who can watch the video. Defaults to private on purpose: the run refuses to press Publish unless it can read the chosen option back off the radio, because a video that should have been private going public cannot be undone.

## `madeForKids` (type: `boolean`):

Set this only if the video is directed to children. This is the COPPA audience declaration and it is legally binding on you, not on this Actor. It is also the one answer that gates Studio's Next button, so it is always set explicitly and read back.

## `channelId` (type: `string`):

Optional. The UC… channel id to upload to, e.g. UCXXXXXXXXXXXXXXXXXXXXXX. Not the @handle and not the Brand-account id. If the signed-in account does not manage this channel, Studio silently redirects to the account's default channel — the run detects that and stops instead of uploading to the wrong place. Leave blank to use the session's default channel.

## `thumbnailUrl` (type: `string`):

Optional. Public https direct URL to a JPG/PNG/WEBP thumbnail, under YouTube's own 2 MB limit. Best-effort: custom thumbnails require a phone-verified channel, and on an unverified channel Studio accepts the click and silently does nothing. The result row reports whether it was applied.

## `channelIsPhoneVerified` (type: `boolean`):

Tick this only if the channel has completed phone verification at youtube.com/verify. Without verification YouTube caps uploads at 15 minutes — and it accepts the whole file first and rejects it for length afterwards, so a 4 GB transfer can be paid for and thrown away. With this unticked, anything over 15 minutes is refused — normally on the first megabyte, read over a range request before the transfer, though a container the probe cannot read is checked after the transfer instead. One phone number can only verify 2 channels per year.

## `publishAt` (type: `string`):

Optional. RFC 3339 timestamp WITH an explicit UTC offset, e.g. 2026-09-01T14:00:00-07:00. The offset is mandatory: without one the schedule lands in whatever timezone Studio happens to be showing, and YouTube renders published dates in Pacific Time on the watch page, so an ambiguous time reads back as the wrong day. A scheduled video is private until its publish moment, so visibility is set to private for you and the log says what it will become and when.

## `categoryId` (type: `string`):

The YouTube category for the video. Set in Studio's advanced pane.

## `playlistId` (type: `string`):

Optional. Add the video to this playlist. Best-effort — Studio's playlist picker is a search dropdown, so if the playlist cannot be matched the upload still completes and the result row says so.

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

PASTE THIS THE FIRST TIME, THEN LEAVE IT EMPTY. Anything you paste here WINS: the run signs in with it and does not read the saved session at all. Leave the field empty and the run uses the session saved by an earlier run instead. With nothing pasted AND no saved session, the run returns the labelled sample row and uploads nothing. Paste again whenever you want to switch Google account or recover — a paste always overrides what is stored, so a saved session can never publish to an account you did not just name. HOW TO EXPORT: paste TWO Cookie-Editor JSON exports here, one after the other (order does not matter, and you do not need to merge them yourself). (1) On https://www.youtube.com/robots.txt while signed in: Cookie-Editor > Export > JSON. robots.txt because it is a plain text page with nothing loading behind it, so no background request can re-mint a cookie between the export and the paste. (2) On https://myaccount.google.com/ in the same window: Export > JSON again. BOTH are needed — Cookie-Editor can only ever export the domain of the tab you are on, and the upload flow passes through accounts.google.com, so a youtube.com-only export arrives there with nothing to show and Google draws its sign-in page. Chrome is the tested path: before exporting, set the extension to "On all sites" in chrome://extensions > Cookie-Editor > Details, or it silently omits every non-Secure cookie (SID, HSID, APISID). Export and run within a few minutes: Google re-mints the rotating cookies about every 10 minutes, and an export is good for one use only — do not test the same jar elsewhere first. Close the window afterwards, but do NOT sign out; signing out kills the session for every copy. WHAT GETS SAVED: once a run authenticates AND reads back which channel it is signed into, this Actor stores the refreshed cookie jar in a key-value store named "youtube-uploader-sessions" in YOUR OWN Apify account, never the developer's and never in a run's output store. A run that signs in and cannot read the channel back saves nothing and says so in the log. Later runs open Chrome on what was saved. It does not expire; to delete it, tick "Forget the saved session" (resetProfile) together with a fresh export, or delete that store in Apify Console > Storage — which does not sign the Google account out.

## `resetProfile` (type: `boolean`):

A RECOVERY HATCH, not the way to switch accounts. To switch accounts you only need to paste the new account's export — anything you paste already wins over the saved session, which is never consulted on a run that supplies cookies. Tick this box only when the SAVED session itself has gone bad and you want it gone: the account was signed out everywhere, or the password changed. It deletes the saved cookie jar for this channel — the one record this Actor stores — before the run reads anything. YOU MUST PASTE A FRESH COOKIE EXPORT IN THE SAME RUN. Ticking this on its own does nothing at all: that run would have no session to sign in with, so it returns the sample row and deletes nothing — a run that never opens a browser must not be able to destroy your saved session. Untick the box and empty the cookies field again afterwards.

## `SID` (type: `string`):

In your browser: DevTools > Application > Cookies > https://www.youtube.com, find the row named SID and copy its Value column. Paste just the value. It must come from the .youtube.com list, NOT the .google.com one — Studio never receives google.com cookies.

## `HSID` (type: `string`):

From the same .youtube.com Cookies list. This one is HttpOnly, so a JavaScript/console cookie dump will not contain it — you need a cookie-manager extension (Cookie-Editor / Cookie Quick Manager).

## `SSID` (type: `string`):

From the same .youtube.com Cookies list. HttpOnly, like HSID, so a console cookie dump will not contain it either.

## `APISID` (type: `string`):

From the same .youtube.com Cookies list. Paste just the value.

## `SAPISID` (type: `string`):

Load-bearing beyond page load: Studio's own JavaScript derives the `Authorization: SAPISIDHASH` header from this value, and every Studio request 401s without it even though the page renders.

## `__Secure-1PSID` (type: `string`):

The first-party session id. HttpOnly.

## `__Secure-3PSID` (type: `string`):

The third-party-context session id. Studio specifically needs this one — an export filtered to first-party cookies only produces a session that looks complete and still bounces to the sign-in page.

## `LOGIN_INFO` (type: `string`):

YouTube's own logged-in-identity cookie. Studio bounces to a sign-in without it even when the whole SID family is intact. It is also the cheapest single proof that your export came from a real cookie API rather than a console dump — it is HttpOnly, so a `document.cookie` paste can never contain it.

## `__Secure-1PSIDTS` (type: `string`):

The rotating freshness partner of \_\_Secure-1PSID (TS = timestamp). Google re-mints it roughly every 10 minutes. Its absence makes Google treat the session id as unbound — i.e. stolen-looking — and redirect to sign-in. Export it fresh, minutes before the run.

## `__Secure-3PSIDTS` (type: `string`):

The same, for \_\_Secure-3PSID. Google re-mints it roughly every 10 minutes, and a session id with no matching PSIDTS is treated as unbound. Export it fresh, minutes before the run.

## `__Secure-1PAPISID` (type: `string`):

Backs the SAPISID1PHASH Authorization scheme.

## `__Secure-3PAPISID` (type: `string`):

Backs the SAPISID3PHASH Authorization scheme. YouTube falls back to this when SAPISID is missing.

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

Egress for the Studio session. The proxy GROUP you pick here is IGNORED — this Actor chooses its own datacenter lane in code so a run can never be redirected onto a metered group. Two things here do take effect: (1) the COUNTRY, which is worth matching to where you exported your cookies, though that is no guarantee — a full run through an exit in the account's own country met the same verification challenge, so what actually triggers it is not established; and (2) your OWN servers in "Proxy URLs", which are used exactly as given. Switching the Apify proxy off with no servers of your own keeps the run direct. The video file itself is never fetched through this connection — it is downloaded straight from the URL you gave.

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

Force a labelled SAMPLE run that uploads nothing and moves no video bytes, even with a session available. A run with no cookies pasted AND no saved session is a sample run too. Once a session is saved, two things reach the sample row: this box, or "Forget the saved session" ticked with the cookie box left empty.

## Actor input object example

```json
{
  "tags": [],
  "visibility": "private",
  "madeForKids": false,
  "channelIsPhoneVerified": false,
  "categoryId": "22",
  "resetProfile": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "BUYPROXIES94952"
    ]
  },
  "demoMode": false
}
```

# Actor output Schema

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

One row per run: the video id, watch URL, the settings that were read back and confirmed, per-step warnings, and the size the run was billed on. A run that did not publish writes one row instead, carrying a stable errorCode, a plain-English error and a hint. Every code is listed in the README, in the two tables under "What it refuses".

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

A screenshot and a copy of the page HTML from any failed step, plus CHARGE\_RESULT, the record of what this run actually charged. The HTML is SANITISED before storing — inline script bodies, session cookie names and values, and token-like strings are replaced with placeholders. The screenshot is not sanitised and cannot be: it is a picture of a signed-in Studio page, so it shows the account name and avatar. That is your own account data in your own store — treat it the way you would treat a screenshot of your own browser before you share the store or its id. No session cookie is written here: the rolling session lives in the youtube-uploader-sessions key-value store in your own account.

# 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/youtube-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/youtube-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/youtube-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/youtube-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/RnLolvvUcRi2WFF84/builds/ruAa40fxUcKpztfRJ/openapi.json
