Uploading a video to YouTube from code is a solved problem. One resumable POST, a list of parts, an OAuth token. You can have it working before lunch.
The trouble starts at the second network. Your editor exported one render. YouTube will take it as-is. TikTok will not take bytes at all — it pulls from a URL you host. Instagram will run it as a Reel for up to fifteen minutes but rejects it from the feed past sixty seconds. Vimeo wants a title and refuses a post that has no video in it. Four networks, four clients, four error vocabularies, and — the part that catches people — four encodes of the same file.
This post is both halves: how the YouTube upload API actually works, and what it takes to stop writing the fourth integration.
The YouTube upload API, concretely
videos.insert is a media-upload method, so it has a separate upload URI from the rest of the Data API. The normal shape is a resumable session: you POST the metadata, get a session URL back in the Location header, then PUT the bytes at it.
1# 1. Open a resumable session. The metadata goes in the body.
2curl -X POST \
3 "https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status" \
4 -H "Authorization: Bearer $GOOGLE_ACCESS_TOKEN" \
5 -H "Content-Type: application/json" \
6 -H "X-Upload-Content-Type: video/mp4" \
7 -H "X-Upload-Content-Length: 84213760" \
8 -D - \
9 -d '{
10 "snippet": {
11 "title": "We rebuilt the editor",
12 "description": "90 seconds on what changed.",
13 "tags": ["changelog", "editor"],
14 "categoryId": "28"
15 },
16 "status": {
17 "privacyStatus": "public",
18 "selfDeclaredMadeForKids": false
19 }
20 }'
21
22# 2. PUT the bytes at the URL returned in the Location response header.
23curl -X PUT "$RESUMABLE_SESSION_URL" \
24 -H "Content-Type: video/mp4" \
25 --data-binary @render.mp4The constraints, from Google's reference:
- Scopes — one of
youtube.upload,youtube,youtube.force-ssloryoutubepartner. - Maximum file size — 256 GB. Accepted media MIME types are
video/*andapplication/octet-stream. - Settable parts —
snippet,status,contentDetails,recordingDetails,localizationsand a dozen more. You only needsnippetandstatusto publish.
Two things that will surprise you
Uploads have their own daily bucket, and it holds 100 calls. Google's quota page is explicit: "Projects that enable the YouTube Data API have a default quota allocation of 100 search.list calls, 100 videos.insert calls, and 10,000 units per day combined for all other endpoints." This is not 10,000 units divided by an upload price — it is a hard ceiling of 100 uploads, separate from everything else you do. Getting past it means the compliance audit, which we walk through in YouTube API pricing and quota.
Until your project is verified, every upload is private. Again verbatim: "All videos uploaded via the videos.insert endpoint from unverified API projects created after 28 July 2020 will be restricted to private viewing mode." Your privacyStatus: "public" is accepted and then quietly ignored. TikTok does the same thing to unaudited clients — we covered that gate in the TikTok Content Posting API.
Neither of those is a code problem, which is why neither gets solved in an afternoon. Budget weeks of review, per platform, before your first public video.
The fifth network is where one problem becomes five
Here is what the same file runs into, network by network. These numbers are the ones we enforce, and each row is checkable in our configuration docs.
Network | Accepts | The limit that actually bites |
|---|---|---|
YouTube |
| 100 uploads a day per project; private until the project is verified |
TikTok | MP4 (H.264), MOV, WEBM | Duration is per creator — read |
Instagram feed | MOV or MP4, H264 or HEVC, AAC audio | 3 to 60 seconds, 100 MB, 23–60 FPS |
Instagram Reels | MOV or MP4 | 3 seconds to 15 minutes; 9:16 recommended |
Vimeo | Exactly one video | A post with no video is rejected, and extra media containers are disregarded |
Read that table as a single file's obstacle course. One 90-second 4K render is a legal YouTube upload, an illegal Instagram feed video, a legal Reel, and a TikTok upload whose legality depends on whose account it is going to. "Just call four APIs" undersells the work by an order of magnitude: you need four renditions before you need four clients.
One upload, one request
Outstand splits this into media once, then a single post. Four HTTP requests end to end, and three of them are the file.
Step 1 — put the file where every network can reach it
1# a. Ask for a presigned URL.
2curl -X POST "https://api.outstand.so/v1/media/upload" \
3 -H "Authorization: Bearer $OUTSTAND_API_KEY" \
4 -H "Content-Type: application/json" \
5 -d '{ "filename": "render.mp4", "content_type": "video/mp4" }'
6
7# { "success": true,
8# "data": { "id": "9dyJS", "upload_url": "https://...", "expires_in": 3600 } }
9
10# b. PUT the bytes. The presigned URL is good for one hour.
11curl -X PUT -T render.mp4 -H "Content-Type: video/mp4" "$UPLOAD_URL"
12
13# c. Activate it. The file is not usable in a post until this returns.
14curl -X POST "https://api.outstand.so/v1/media/9dyJS/confirm" \
15 -H "Authorization: Bearer $OUTSTAND_API_KEY" \
16 -H "Content-Type: application/json" \
17 -d '{}'
18
19# { "success": true,
20# "data": { "id": "9dyJS", "url": "https://media.outstand.so/...",
21# "status": "active", ... } }Keep the url from the confirm response, not the id. The Create a post reference is blunt about this: each media object is { url, filename } where the url "is the publicly accessible URL returned by the confirm-upload step… media is not referenced by id." The id is how you confirm and delete the file; the url is how you post it.
Step 2 — one POST, four networks
1curl -X POST "https://api.outstand.so/v1/posts" \
2 -H "Authorization: Bearer $OUTSTAND_API_KEY" \
3 -H "Content-Type: application/json" \
4 -H "Idempotency-Key: b1f6c3f4-8f0e-4a5b-9c2d-7e1a0b3c4d5e" \
5 -d '{
6 "accounts": ["Kx7vQ", "Tm2bN", "Rd8pL", "Vz4mH"],
7 "containers": [
8 {
9 "content": "We rebuilt the editor. 90 seconds on what changed.",
10 "media": [
11 {
12 "url": "https://media.outstand.so/...",
13 "filename": "render.mp4"
14 }
15 ]
16 }
17 ],
18 "processMedia": true,
19 "youtube": {
20 "title": "We rebuilt the editor",
21 "categoryId": "28",
22 "privacyStatus": "public",
23 "madeForKids": false,
24 "tags": ["changelog", "editor"]
25 },
26 "tiktok": {
27 "postMode": "DIRECT_POST",
28 "privacyLevel": "SELF_ONLY"
29 },
30 "instagram": {
31 "reelCoverUrl": "https://cdn.example.com/cover.jpg"
32 },
33 "vimeo": {
34 "title": "We rebuilt the editor",
35 "privacyView": "anybody",
36 "privacyEmbed": "public"
37 }
38 }'processMedia is the line doing the expensive work. From the reference: "When true, video media is inspected and re-encoded to each target network's requirements (container, codec, resolution, frame rate and file size) before publishing, and the correct rendition is sent to each network." Leave it unset and the file publishes exactly as supplied, which is what you want if your pipeline already produces per-network renders. It is billed per rendition produced — inspection and validation are always free — and it does nothing at all on a post without video.
That is the whole path: presigned URL, PUT, confirm, post. Four requests to put one render on four networks, and no encoder of your own.
The per-network blocks, field by field
Everything network-specific lives in a top-level object named after the network. The text in containers[].content is the fallback; a content key inside a network's block overrides it for that network only.
youtube—title,categoryId,privacyStatus(public,private,unlisted),isShort,madeForKids,containsSyntheticMedia,tags.tiktok—postMode,privacyLevel,disable_comment.instagram—publishAsStory,reelCoverUrl,reelThumbOffset,isAiGenerated.vimeo—title,description,privacyView,privacyEmbed.
The TikTok default is not a publish. If you omit postMode you get MEDIA_UPLOAD, which delivers the media to the creator's TikTok inbox as a draft for them to caption and post themselves. Those posts have no public URL and no analytics until the creator finishes them — platformPostUrl is null, analytics return an error saying so, and TikTok expires the publish record after about 24 hours. DIRECT_POST publishes straight to the profile, but it draws on TikTok's active creator cap: five distinct creators in a rolling 24 hours for an unaudited client.
That is also why the example above sets privacyLevel to SELF_ONLY. An unaudited client cannot Direct Post publicly at all — TikTok returns unaudited_client_can_only_post_to_private_accounts. Once you are audited, fetch the creator's privacy_level_options and pick from it; anything else comes back as privacy_level_option_mismatch.
Vimeo, the one you can check for yourself
Video teams keep a Vimeo presence for the things that do not belong on YouTube — client review links, embeds on a marketing site, portfolio work. It is also the network almost no unified API carries.
Ayrshare's homepage lists Facebook, X, Bluesky, Instagram, LinkedIn, Reddit, Telegram, TikTok, Google Business Profile, Threads, Pinterest, Snapchat, YouTube and WhatsApp in beta. Zernio lists sixteen: Instagram, TikTok, YouTube, X, LinkedIn, Facebook, Threads, Pinterest, Reddit, Bluesky, WhatsApp, Telegram, Discord, Snapchat and Google Business. Neither list contains Vimeo. Ours does, and you can verify all three claims in about a minute.
Two honest caveats. Vimeo is BYOK — Managed Keys do not cover it, so you register a Vimeo app and supply your own client credentials. And Vimeo itself gates upload access on free accounts behind a manual approval that their docs say can take up to five business days. The integration is real; the onboarding is not instant. The Vimeo configuration guide has the scope list.
Three things that will bite you
- An account identifier that resolves to nothing is dropped silently. Quoting the reference: the request "is rejected with 400 only when NONE of the supplied identifiers resolve; if at least one resolves, the unresolved entries are silently ignored." A typo in one of four accounts gives you a 200 and a three-network post. Compare the returned
socialAccountsarray against what you sent, every time. - Unknown keys inside a network block are warnings, not errors. A successful response carries them in a
warningsarray. Log it or you will never see it. - You cannot schedule further than 30 days out. A later
scheduledAtis rejected with 400 and no post is created — so a quarterly calendar has to be held on your side and submitted in windows.
1{
2 "success": true,
3 "warnings": [
4 "Ignored unrecognised key 'post_mode' in 'tiktok'. Did you mean 'postMode'?"
5 ],
6 "post": {
7 "id": "9dyJS",
8 "socialAccounts": [ ... ],
9 "containers": [ ... ]
10 }
11}Knowing it actually landed
Two webhooks matter here. post.published fires "when a post is successfully published to at least one social account." post.error fires "when a post fails to publish to all targeted social accounts."
Read those definitions carefully, because the gap between them is where video lives. A render that YouTube accepted and Instagram rejected for running 70 seconds in the feed is a partial success — it arrives as post.published, not as an error. Both payloads carry event, timestamp and data with postId, orgId and a socialAccounts array, where each entry has either platformPostId and platformPostUrl or an error. Iterate that array. Do not branch on the event name alone.
What this does not do
Worth saying plainly, because it decides whether the above fits your pipeline:
- There is no direct messages API and no unified engagement inbox. Comments and replies are supported per post; DMs are not.
- There is no bulk create. One post per request, though a single request can carry several containers and every network you target.
- There is no built-in approval workflow. If a human signs off before publish, you hold that state and call the post endpoint at the end of it.
FAQ
What is the YouTube upload API?
It is the videos.insert method of the YouTube Data API v3, which uploads a video file and its metadata in one call against https://www.googleapis.com/upload/youtube/v3/videos. Resumable upload is the right mode for anything large enough to fail halfway.
Does a YouTube upload really cost 1,600 quota units?
Not any more. videos.insert now has its own bucket: 100 calls a day, each costing 1, separate from the 10,000 units a day shared by everything else. The practical ceiling on a default project is 100 videos, not a unit budget.
Can I send one file to YouTube, TikTok, Instagram and Vimeo?
Yes, in one POST /v1/posts with a per-network override block for each. Set processMedia: true and the file is re-encoded per network before publishing, so you do not maintain an encoder for four sets of rules.
Why is my uploaded video private when I asked for public?
Because the Google Cloud project has not been verified. Any project created after 28 July 2020 has public uploads forced to private until it passes verification. The API accepts the parameter and the platform overrides it.
Does Outstand support Vimeo?
Yes, with your own Vimeo app credentials — Managed Keys do not extend to Vimeo. Neither Ayrshare nor Zernio lists Vimeo among their supported networks.