# YouTube Uploader Pro - Shorts (`dami_studio/youtube-shorts-uploader-pro`) Actor

Publish a vertical video to your own YouTube channel as a Short, from a URL. You bring your own session cookies, so your login stays yours. Reads the frame size and duration from the first megabyte where the host allows it, so most bad files are refused before the rest transfers.

- **URL**: https://apify.com/dami\_studio/youtube-shorts-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 short 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 — Shorts

Publishes a vertical clip to your own YouTube channel as a Short.

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 upload wizard, and hands you back the video id and the Shorts URL.

Whether YouTube files something as a Short is decided by the file, not by you. The frame has to be square or taller, and the clip has to be 180 seconds or less. There is no toggle for it, and `#Shorts` in the title changes nothing. So this Actor reads your file before it transfers it, and stops early if the file would not qualify. One kind of file cannot be read that way, and for that one the decision lands after the transfer — there is a section below on which.

### What you need

- A public direct link to the .mp4. 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 that session 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.

There are two records: the refreshed cookie jar, and a compressed copy of the browser profile directory. Both live 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. Together those two records are a working signed-in session for that Google account, so treat them the way you treat the cookie export itself.

They do not expire. Three things clear them:

- Tick **Forget the saved session** (`resetProfile`) and paste a fresh cookie export in the same run. Both records are deleted before anything reads them, 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 the store yourself in Apify Console → Storage → Key-value stores → `youtube-uploader-sessions`. The cookie jar is named `session-<channelId>` and the profile archive `profile-<channelId>.tar.gz`, with `default` in place of the id 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. The stored profile is also dropped when Chrome starts on it and then goes away, and the run falls back to the cookie jar once.

To switch accounts you only need to paste. Anything you paste wins outright, and the saved session is not consulted on that run.

Deleting a 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 first.

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

### 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 |
|---|---|
| `SHORTS_TOO_LONG` | Longer than 180 seconds. Past that it stays long-form however you tag it. |
| `SHORTS_NOT_VERTICAL` | The frame is landscape. A landscape file never becomes a Short at any length. |
| `SHORTS_SQUARE_SOURCE` | Exactly 1:1. It does become a Short, but with about 40% of the frame as black bars. Tick **Allow square (1:1) source** if you meant it. |
| `SHORTS_ROTATION_METADATA` | Stored landscape and only looks vertical because of a rotation flag. YouTube reads the stored frame, so this publishes as a normal video without warning. |
| `SHORTS_RESOLUTION_TOO_HIGH` | Short edge above 1080 px, which is YouTube's stated Shorts maximum. No viewer receives those extra pixels. Tick **Allow above-target resolution** to send it anyway. |
| `SHORTS_COPYRIGHT_CLIFF` | Over 60 seconds and you ticked **Contains third-party audio**. Past a minute, any active claim blocks the Short worldwide, after it is already published. |
| `UNPROBEABLE_CONTAINER` | Not an MP4 or MOV, so the duration and frame size cannot be read — and those two numbers are the whole Shorts decision. WebM, Matroska, AVI, FLV, Ogg, WMV and the MPEG streams all land here, and all of them are refused on the first megabyte of the file. Re-encode to MP4/H.264/AAC. |
| `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. |
| `NO_VIDEO_TRACK` | Audio-only file. YouTube cannot make a video out of an MP3. |
| `OVERSIZED_VIDEO` | Three things fire this and the message says which. (1) The file is above the 512 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 does that silently when the account does not manage the channel, so the run stops instead of publishing to the wrong place. |
| `VISIBILITY_UNCONFIRMED` | The visibility setting could not be read back off the page, so the run refused to press Publish and saved a draft. Clicking a radio button and having it selected are two different things, and the gap between them is a private video going public. |
| `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, or a **Maximum total charge (USD)** too low to cover the publish fee. |

Two things get a warning but not a refusal: an MP4 whose `moov` atom sits at the end of the file (a common cause of "Processing abandoned" — fix it with `ffmpeg -i in.mp4 -c copy -movflags +faststart out.mp4`), and a Short over 60 seconds with third-party audio left unticked.

Titles over 100 characters and descriptions over 5000 are cut to fit, and the log says by how much.

#### 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. |
| `UNSUPPORTED_CONTAINER` | The file arrived intact and its opening bytes name a container this Actor does not upload. It is the rare sibling of `UNPROBEABLE_CONTAINER`: it only appears when the first-megabyte probe could not run at all, so the container was named once the file was already on disk. |
| `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 Short 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, so a landscape clip is caught for a megabyte rather than five hundred. If that megabyte does not carry what the checks need, one more is read from the end of the file. 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 and refused there, with `UNPROBEABLE_CONTAINER`. WebM, Matroska, AVI, FLV, Ogg, WMV and the MPEG streams each carry a fixed signature in their opening bytes, and no later byte can turn one of them into an MP4, so the rest of the file never moves. 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.

One kind of file still falls through: 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 `SHORTS_*` 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 512 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.

### Input

| Field | Required | Notes |
|---|---|---|
| `videoUrl` | for real uploads | Public https direct link to the .mp4. You host it. |
| cookies | first run only | Both exports, in the one field. 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 Short called `video.mp4`. |
| `description` | no | Cut to 5000 characters, loudly. |
| `tags` | no | Joined and trimmed to YouTube's 500-character tag field. A minor signal on Shorts — the title and the first few frames do far more. |
| `visibility` | no | `public`, `unlisted` or `private`. Defaults to private. |
| `madeForKids` | no | The COPPA audience declaration. Binding on you, not on this Actor. |
| `channelId` | no | The `UC…` id, not an `@handle` and not a Brand-account id. Use it when the account manages more than one channel: the run stops with `WRONG_CHANNEL` rather than publishing to the wrong one. |
| `thumbnailUrl` | no | Best-effort. Public link to a JPG or PNG. Custom thumbnails need a phone-verified channel; on an unverified one Studio takes the click and does nothing, and the row says so. |
| `allowSquare` | no | Uploads a 1:1 source instead of refusing it. It does become a Short, with roughly 40% of the frame as black bars. |
| `allowAboveTargetResolution` | no | Uploads a source whose short edge is above 1080 px. No viewer receives those pixels. |
| `containsThirdPartyAudio` | no | Tick it when the music or audio is not yours. It turns a Short over 60 seconds into a refusal instead of a warning, because past a minute an active claim blocks the Short worldwide after it is already published. |
| `verifyShortsClassification` | no | On by default. After publishing, polls the watch URL for up to 45 seconds to see whether YouTube filed it under `/shorts/`. An unresolved poll reports `null`. |
| `resetProfile` | no | Deletes the saved session 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 row even with a session available. |

`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.

### Example input

```json
{
  "videoUrl": "https://cdn.example.com/clip-42.mp4",
  "title": "The one-take pancake flip",
  "description": "Filmed on a Tuesday.\n\n#cooking",
  "tags": ["cooking", "kitchen", "breakfast"],
  "visibility": "private",
  "channelId": "UCXXXXXXXXXXXXXXXXXXXXXX"
}
```

### Output

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

```json
{
  "ok": true,
  "billable": true,
  "format": "short",
  "published": true,
  "confirmed": true,
  "videoId": "dQw4w9WgXcQ",
  "shortsUrl": "https://www.youtube.com/shorts/dQw4w9WgXcQ",
  "watchUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "visibility": "public",
  "visibilityConfirmed": true,
  "classifiedAsShort": true,
  "durationSec": 42.5,
  "width": 1080,
  "height": 1920,
  "videoMegabytes": 25,
  "sessionRefreshed": true,
  "warnings": []
}
```

The row also carries `platform`, `description`, `tags`, `madeForKids`, `channelId`, `thumbnailApplied`, `draftSaved`, `checksState`, `studioEditUrl`, `videoBytes`, `egress`, `note`, `publishedAt` and `runId`.

`classifiedAsShort` is either `true` or `null`, never `false`. Classification is asynchronous and YouTube publishes no timing guarantee, so a poll that did not resolve in 45 seconds is an unknown rather than a failure.

`thumbnailApplied: false` on an otherwise good row means Studio ignored your thumbnail. That happens on channels that are not phone-verified.

A watch page showing only 360p right after the run is normal. Higher resolutions fill in over the following hours.

A run that did not publish still finishes as succeeded and writes one row with a stable `errorCode`, a plain-English `error`, 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 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

This drives the real Studio interface, so a YouTube layout change can invalidate a selector. When that happens the run stops with `LAYOUT_CHANGED`, naming the step it was on, and saves the screenshot and the scrubbed HTML. It never reports a success it did not see. Whether it was a redesign, a page that was still rendering, or something the account ran into is not something the run can tell apart from that side — send the run id along with the screenshot and the selectors get checked. If it died mid-wizard it clicks **Save and close** first, so you are left with one recoverable draft rather than an abandoned partial upload.

The other code worth knowing is `DAILY_LIMIT`. It is a per-channel 24-hour cap and YouTube has never published a number for it, 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 Shorts need that disclosure, set it by hand in Studio afterwards.

# Actor input Schema

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

Public direct https:// URL to the vertical .mp4 to publish as a Short (S3, CDN, key-value store — anything that serves the file itself, not an HTML landing page). Required for a real upload. To see the sample row instead, run it with the cookie field blank on an account that has no saved session yet, or tick "demoMode". The file is checked first: it must be square or taller and 180 seconds or less, or YouTube will not file it as a Short. That check normally runs on the first megabyte, read over a range request before the transfer. A container the probe cannot read, or a host that answers a ranged request with the whole file, is checked after the transfer instead — see the README.

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

The Short's title, capped at YouTube's 100 characters (a longer one is cut and the cut is reported in the log, never silently). Required for a real upload: YouTube pre-fills the title box with the video's filename, so a blank title publishes a Short called "video.mp4". You do not need #Shorts — classification is decided by aspect ratio and duration, not by the title.

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

The Short's description, capped at YouTube's 5000 characters. Keep hashtags to 15 or fewer: past 60 hashtags across the title and description YouTube ignores all of them and can drop the video from search.

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

Optional keyword tags. YouTube caps the whole tag field at 500 characters, so the list is joined and trimmed to fit. Tags are a minor ranking signal on Shorts — the title and the first frames matter far more.

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

Who can see the Short. Defaults to Private on purpose, so nothing is ever published publicly by accident. The actor reads the selected radio back off Studio before it presses Publish; if it cannot confirm the setting it saves a draft and refuses to publish rather than risk a private video going public.

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

YouTube's COPPA audience declaration. Leave unticked for content not made for children. This is a legal declaration about your own content — the actor sets what you choose here and verifies the radio took.

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

Optional. The UC... id of the channel to upload to (24 characters starting with UC — not an @handle, not a Brand-account id). Use it when the signed-in account manages several channels: Studio silently redirects to the account's default channel when it does not manage the one you asked for, and the actor stops with WRONG\_CHANNEL rather than uploading to the wrong place. Leave blank to use the channel the session lands on.

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

Optional, best-effort. Public https:// URL to a JPG/PNG thumbnail. Custom thumbnails require a phone-verified channel; on an unverified channel Studio silently ignores the upload, and the result row reports thumbnailApplied: false instead of pretending it worked.

## `allowSquare` (type: `boolean`):

A 1:1 video DOES become a Short — YouTube's criteria say "square or vertical" — but it plays with roughly 40% of the Shorts frame as black bars. A square source is refused by default so that the black bars are something you chose rather than something you find on the published Short, and the refusal normally lands on the first megabyte, before the transfer. Tick this to upload it as-is.

## `allowAboveTargetResolution` (type: `boolean`):

YouTube states "You can upload Short videos with a maximum resolution of 1080p". A source whose short edge is above 1080 px is refused by default, because every pixel above that is bandwidth you are billed for and quality no viewer receives. Tick this to upload it anyway.

## `containsThirdPartyAudio` (type: `boolean`):

Tick this if the Short uses music or audio you did not create. Over 60 seconds, YouTube's rule is absolute: any Short with an active copyright claim of any type — including manual claims — is blocked GLOBALLY, after publishing. With this ticked, a Short longer than 60 seconds is refused, normally on the first megabyte and before the transfer. With it unticked, the run continues and logs the risk.

## `verifyShortsClassification` (type: `boolean`):

After publishing, poll the watch URL for up to 45 seconds to see whether YouTube has redirected it to /shorts/. Classification happens asynchronously with no published timing guarantee, so an unresolved poll reports null rather than a failure. The video is published either way.

## `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 that session — the refreshed cookie jar plus a gzipped copy of the Chrome profile directory (cookies, Local Storage, IndexedDB, service workers, device identifiers) — 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 it already signed in. 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, the password changed, or the profile was written by a Chrome build this image cannot open. It deletes BOTH saved records for this channel — the browser profile and the rolling cookie jar — 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,
  "allowSquare": false,
  "allowAboveTargetResolution": false,
  "containsThirdPartyAudio": false,
  "verifyShortsClassification": true,
  "resetProfile": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "BUYPROXIES94952"
    ]
  },
  "demoMode": false
}
```

# Actor output Schema

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

One row per run: the published Short's id and URLs, the settings that were read back and confirmed, 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-shorts-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-shorts-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-shorts-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-shorts-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/SAoBklcJRplwG6dDZ/builds/VEm7YfkgFoYo1MXpy/openapi.json
