Skip to main content
API reference

Quick start · YouTube

YouTube WebSocket API quick start for new uploads

Connect to your YouTube stream and receive an event for every new upload and Short from the channels you track. This page gets a consumer running; the API reference holds every message type.

From $150/month for 5 channels · WebSocket +$100/month · Under a second
WebSocket
ws_url from GET /v1/dashboard
REST base
https://youtube.1322.io
Auth
X-Api-Key or Authorization: Bearer
Updated

What you get

New uploads and Shorts from the YouTube channels on your list. Three message types share one stream: upload (the one to build on, subtype is video or short), upgrade (a better thumbnail for a video you already have) and deletion (a tracked video confirmed unavailable).

Uploads arrive in under a second. You do not need a YouTube Data API key or quota of your own.

Price: From $150/month for 5 channels; WebSocket +$100/month. Plans: Lite $150/month (5 channels), Standard $300/month (15 channels), Premium $400/month (50 channels). Pay monthly, or prepay 3 months (10% off), 6 months (15% off) or a year (25% off). See YouTube pricing.

Authentication

  • REST: send your API key as X-Api-Key: <api_key> or Authorization: Bearer <api_key>.
  • GET /v1/dashboard returns a complete ws_url and a separate ws_key. Connect to ws_url?key=<ws_key>, or send the key in the X-WS-KEY header instead: one or the other, not both.
  • The API access page in your dashboard shows the same API key, WebSocket URL and WS key.

Connect to the WebSocket

Set these environment variables, then run either consumer from your server:

  • YT_WS_URL: ws_url from GET /v1/dashboard
  • YT_WS_KEY: ws_key from GET /v1/dashboard
  • YT_API_KEY: your API key (REST only)
# pip install "websockets>=13"   (Python 3.9+)
import asyncio, json, os
from websockets.asyncio.client import connect

# ws_url and ws_key from GET /v1/dashboard (or the API access page)
WS_URL = os.environ["YT_WS_URL"] + "?key=" + os.environ["YT_WS_KEY"]

def handle(msg):
    kind = msg.get("type")
    if kind == "upload":
        video = msg.get("video") or {}
        channel = msg.get("channel") or {}
        print(msg.get("subtype"), channel.get("name"), video.get("title"), video.get("url"))
    elif kind == "upgrade":
        upgrade = msg.get("upgrade") or {}
        print("better thumbnail for", upgrade.get("video_id"), upgrade.get("url"))
    elif kind == "deletion":
        print("deleted", (msg.get("video") or {}).get("id"))

async def main():
    backoff = 1
    while True:
        try:
            async with connect(WS_URL) as ws:
                backoff = 1
                async for raw in ws:
                    try:
                        event = json.loads(raw)
                    except ValueError:
                        continue
                    if isinstance(event, dict):
                        handle(event)
        except Exception as exc:
            print("disconnected:", exc)
        await asyncio.sleep(backoff)
        backoff = min(backoff * 2, 30)

asyncio.run(main())
// npm install ws, save as consumer.mjs, run: node consumer.mjs   (Node 18+)
import WebSocket from "ws"

// ws_url and ws_key from GET /v1/dashboard (or the API access page)
const BASE_URL = process.env.YT_WS_URL
const WS_KEY = process.env.YT_WS_KEY
if (!BASE_URL || !WS_KEY) throw new Error("Set YT_WS_URL and YT_WS_KEY")
const WS_URL = BASE_URL + "?key=" + encodeURIComponent(WS_KEY)

function handle(msg) {
  if (msg.type === "upload") {
    console.log(msg.subtype, msg.channel?.name, msg.video?.title, msg.video?.url)
  } else if (msg.type === "upgrade") {
    console.log("better thumbnail for", msg.upgrade?.video_id, msg.upgrade?.url)
  } else if (msg.type === "deletion") {
    console.log("deleted", msg.video?.id)
  }
}

let backoff = 1000

function connect() {
  const ws = new WebSocket(WS_URL)
  ws.on("open", () => { backoff = 1000 })
  ws.on("message", (raw) => {
    let event
    try { event = JSON.parse(raw.toString()) } catch { return }
    if (event && typeof event === "object" && !Array.isArray(event)) handle(event)
  })
  ws.on("error", (err) => console.error("socket error:", err.message))
  ws.on("close", () => {
    setTimeout(connect, backoff)
    backoff = Math.min(backoff * 2, 30000)
  })
}

connect()
  • One UTF-8 JSON object per text frame. The server pings every 30 seconds; both libraries answer automatically.
  • Treat every message as standalone: most videos never produce an upgrade or a deletion, so nothing should wait for one. Events are not replayed while you are offline.

One example frame

Illustrative values for fictional accounts, trimmed to the fields most consumers read first. A real frame carries the full set for your tracked accounts.

{
  "type": "upload",
  "subtype": "video",
  "detected_at": "2026-10-15T14:03:22Z",
  "channel": {
    "id": "UCexampleChannel0000001a",
    "name": "Example Studio",
    "url": "https://www.youtube.com/channel/UCexampleChannel0000001a",
    "avatar": null
  },
  "video": {
    "id": "aBcDeF12345",
    "url": "https://www.youtube.com/watch?v=aBcDeF12345",
    "title": "Studio tour, part 2",
    "description": null,
    "published_at": "2026-10-15T14:03:00Z",
    "updated_at": null
  },
  "images": {
    "seed": null,
    "chosen": "https://example.com/thumbs/aBcDeF12345.jpg",
    "thumbnail_small": null
  }
}
  • Every channel and images field can be null; images.chosen is the best image available when the upload is sent.
  • An upgrade carries upgrade.video_id and a replacement image URL: match it to the upload you already rendered.

Manage the list over REST

REST base https://youtube.1322.io. Each call below sends X-Api-Key with your API key.

MethodPathWhat it does
GET/v1/dashboardStatus, term, slots, tracked channels, ws_url and ws_key (flat).
GET/v1/listJust the tracked channels: channel_id, name, added_at.
POST/v1/trackAdd a channel by UC… ID, @handle or channel URL.
POST/v1/untrackRemove a channel.
GET/v1/limitsTracked count, max_tracked and your per-key request rate (rpm).
curl "https://youtube.1322.io/v1/dashboard" \
  -H "X-Api-Key: $YT_API_KEY"

curl "https://youtube.1322.io/v1/list" \
  -H "X-Api-Key: $YT_API_KEY"

curl -X POST "https://youtube.1322.io/v1/track" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $YT_API_KEY" \
  -d '{ "channel": "@examplestudio" }'

curl -X POST "https://youtube.1322.io/v1/untrack" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $YT_API_KEY" \
  -d '{ "channel": "UCexampleChannel0000001a" }'

curl "https://youtube.1322.io/v1/limits" \
  -H "X-Api-Key: $YT_API_KEY"
  • GET /v1/health is a liveness check for uptime monitors; /openapi.json serves the machine-readable schema.

Full reference

Every field, endpoint and edge case for YouTube lives in the API reference. Jump straight to the part you need:

Common questions

Do upload, upgrade and deletion always arrive together?

No. upload is sent for every detected video. upgrade is sent only when a better thumbnail becomes available, and deletion only when a video is confirmed unavailable, at any later time or never.

Can I add a channel by @handle?

Yes. POST /v1/track accepts a UC-prefixed channel ID, an @handle or a channel URL; they resolve to the same channel.

Other quick starts

Related: