# Upload a video to YouTube with the API
URL: https://skedcast.com/developers/guides/publish-to-youtube-api

Upload a video to YouTube with one POST to /v1/posts. The title is optional: it falls back to the first line of your caption, but send it explicitly. Category, privacy, the made-for-kids declaration and the license all have defaults. Subtitles, chapters, translations and a playlist are opt-in keys on the same youtube variant.

## What do you need before you publish?
You need one connected account for the platform. Connect it in the SkedCast console. If a client owns the account, create a connect link with `POST /v1/clients/{id}/connect-invites` and send it to them. They authorize from that page without a SkedCast login.
`GET /v1/accounts` lists the connected accounts with their ids. Pass that id in `targets.accountIds` below.

## How do you schedule a YouTube post with the API?
```bash
curl -X POST https://api.skedcast.com/v1/posts \
  -H "Authorization: Bearer sked_live_YOUR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "<clientId>",
    "content": "Launch day is here 🚀",
    "mediaIds": ["<mediaAssetId>"],
    "targets": { "mode": "selection", "accountIds": ["<youtubeAccountId>"] },
    "schedule": { "type": "at", "scheduledAt": "2030-01-15T15:00:00Z" }
  }'
```

## Which YouTube fields can you set?
Put everything YouTube-specific in one `variants` entry with `"platform": "youtube"` and an `overrides` object. SkedCast checks YouTube variants key by key: a mistake in one comes back in the response's `warnings`, or as a `422` for the newer keys.

## Reference
Field | Type | Values / default | Notes
`title` | string (≤100) | — | Optional. Falls back to the first line of `content` when unset. No `<` or `>`.
`description` | string (≤5,000 bytes) | — | Optional. Overrides the caption below its first line, which is used when this is unset. No `<` or `>`.
`privacyStatus` | enum | `public` \| `unlisted` \| `private` | Defaults to `public` when unset.
`categoryId` | enum string | e.g. `"20"` Gaming, `"27"` Education, `"28"` Science & Technology | Defaults to `"22"` (People & Blogs).
`madeForKids` | boolean | `false` | The COPPA self-declaration. Always sent, and `false` unless you set it: it is never read from the channel.
`tags` | string[] or comma-separated string | — | 500 characters in total. A tag containing a space counts two more.
`license` | enum | `youtube` \| `creativeCommon` | Defaults to the Standard YouTube License.
`aiGenerated` | boolean | — | Discloses realistic synthetic or altered media. The post-level `aiDisclosure` copies onto it.
`isShort` | boolean | — | Checks the video against Shorts rules before it publishes: a video over 3 minutes is refused. YouTube itself decides what is a Short.
`playlistId` | string | — | Adds the video to that playlist once it is live. The outcome is `playlistIssue` on the post target.
`notifySubscribers` | boolean | `true` | Notify subscribers about the upload.
`embeddable`, `publicStatsViewable` | boolean | — | Allow embedding; show the like count. Unset leaves YouTube's default.
`defaultLanguage` | BCP-47 tag | — | The language of the title and description. Required when `localizations` is sent.
`localizations` | object | — | `{ "<bcp47>": { "title", "description" } }`, up to 50 languages. Needs `defaultLanguage`.
`recordingDate` | `YYYY-MM-DD` | — | When the video was recorded.
`chapters` | `[{ start, title }]` | — | `start` is whole seconds. YouTube's rules apply: the first at 0, at least 3, each at least 10 seconds long. SkedCast adds them to the description as `m:ss Title` lines.
`captions` | array (≤10) | — | Subtitle tracks `{ language, format: srt \| vtt, text \| mediaId, name?, isDraft? }`. Timed captions only, uploaded once the video is live. Needs the `youtube.force-ssl` scope on the connection.
`firstComment` | string | — | Posted as a top-level comment on the video. Needs the `youtube.force-ssl` scope on the connection. Without it Google answers 403 and the video still publishes. Best-effort: a failed comment never fails the post. The outcome is reported as `firstCommentIssue` on `GET /v1/post-targets/{id}`.

## A mistyped key comes back in warnings
Read `warnings` after every compose. Each entry is `{ code, path, message }` and never blocks the post:
`override_key_unknown`: a key SkedCast does not read. `override_key_wrong_platform`: a key that belongs to another platform. `override_key_deprecated`: an old spelling of a key. `override_value_tolerated`: a wrong type on a key that is accepted anyway, so the value is used as sent. The API reference lists every warning code, including the ones for drafts and edits.
A wrong value on `captions`, `chapters`, `notifySubscribers`, `embeddable`, `publicStatsViewable`, `defaultLanguage`, `localizations` or `recordingDate` is refused with `422 validation_failed` and the field path.

## How do you add YouTube-specific settings?
```json
{
  "clientId": "<clientId>",
  "content": "How bulk scheduling actually works, in 60 seconds.",
  "mediaIds": ["<videoAssetId>"],
  "coverMediaId": "<coverImageAssetId>",
  "targets": { "mode": "selection", "accountIds": ["<youtubeAccountId>"] },
  "variants": [
    {
      "platform": "youtube",
      "overrides": { "title": "Bulk scheduling in 60 seconds", "categoryId": "28", "madeForKids": false }
    }
  ],
  "schedule": { "type": "at", "scheduledAt": "2030-01-15T15:00:00Z" }
}
```

## Native scheduling and Shorts
Set the post-level `scheduleMode: "native"` to let YouTube publish at the scheduled time. It is off by default: with `"managed"`, SkedCast publishes at that time. Facebook supports native scheduling too.
A natively scheduled video is uploaded `private` and YouTube makes it public at the time, so an `unlisted` video cannot be scheduled natively. Schedule it with `managed`.
A Short is a vertical or square video of 3 minutes or less. YouTube decides that from the video itself, so there is no upload flag. `isShort` checks the video against those rules and refuses a longer one before anything is sent.

## Subtitles and chapters
Subtitles are SRT or WebVTT with timing. YouTube requires timed captions, so text without timings is refused. Send the file's content as `text` (up to 512 KiB) or upload the file and send its `mediaId`, one track per language.
SkedCast uploads subtitles after the video is live. The per-track outcome is `publish_effects.captions`, summarised as `captionIssue` on `GET /v1/post-targets/{id}`. A track that fails never fails the post.
YouTube has no chapters field. It builds chapters from a list of timestamps in the description, and only when the list starts at 0:00, has at least three entries and every chapter is at least 10 seconds long. SkedCast checks your `chapters` against those rules and adds them to the description.
Everything after the upload (a subtitle track, the thumbnail, a playlist add, the first comment, an edit) is a separate step. An optional step that YouTube cannot accept at that moment is skipped, and its outcome says so. A synchronous edit can answer `429 youtube_units_exhausted` with `Retry-After`: retry after that time.

## Edit the video after it is published
`POST /v1/posts/{id}/targets/{targetId}/edit-published` changes a video that is already live (or natively scheduled) without republishing it. You can change the title, description, tags, category, privacy, made-for-kids, embedding and statistics settings, default language and translations, the thumbnail and subtitle tracks.
Send only what you are changing. An omitted field is left as YouTube holds it. The thumbnail is `coverMediaId`: a JPEG or PNG up to 50 MB, on a channel verified for custom thumbnails. The MCP tool is `edit_published_post`.
A natively scheduled video's privacy belongs to its schedule, so changing it is `422 edit_privacy_scheduled`. A destination that is not a published or scheduled YouTube video is `422 edit_not_supported`.

## What the API cannot do on YouTube
SkedCast cannot pin or heart a comment, set end screens or cards, publish Community posts, run Test & Compare or set `defaultAudioLanguage`, because YouTube's API does not offer them. SkedCast does not report thumbnail impressions, click-through rate or revenue.

## How do you check that the post published?
Publishing creates one post target per account. `GET /v1/post-targets/{id}` (or `GET /v1/posts/{id}`) reports `status`, plus `externalId` (YouTube's own post id) and `permalink` (its public URL) once published. To avoid polling, subscribe to the `target.published` and `target.failed` webhooks (see the webhooks guide).

## FAQ
**Q: What happens if I don't send a title?**
A: SkedCast falls back to the first line of your content caption. That is a plain-text fallback, not a generated title, so send overrides.title for anything you want as the video's title.

**Q: How do I post a custom thumbnail?**
A: Upload the image as its own media asset (presign, PUT, register, as for any upload) and pass its id as the post-level coverMediaId. YouTube takes a JPEG or PNG up to 50 MB, on a channel verified for custom thumbnails. Omit it to use the extracted poster frame.

**Q: Can I add subtitles in several languages?**
A: Yes: up to 10 tracks in overrides.captions, one per language, as SRT or WebVTT with timing. They upload once the video is live and need the youtube.force-ssl permission. If the connection lacks it, the track is skipped and captionIssue tells you to reconnect.

**Q: Can I change a video after it has published?**
A: Yes, with POST /v1/posts/{id}/targets/{targetId}/edit-published (or the edit_published_post MCP tool). If the edit is refused with 429 youtube_units_exhausted, wait for the Retry-After time and send it again.

**Q: Can the API pin my first comment?**
A: No. YouTube's API cannot pin or heart a comment. SkedCast posts the comment and gives you firstCommentUrl, a link that opens it on YouTube so you can pin it yourself.
