A YouTube webhook is an HTTP callback that fires the moment a channel publishes a video. YouTube doesn't offer one through the Data API — it publishes to a WebSub hub, still widely called PubSubHubbub, and you subscribe to that. It's free and it's worked for years. It's also more work than it sounds: you host a public callback, answer a verification handshake, and re-subscribe every five days. Here's how it works, and where it stops being worth doing by hand.
The YouTube Data API can't push anything. It answers questions when you ask them, and asking it repeatedly for new videos is how people burn through the 10,000-unit daily quota. Push lives somewhere else entirely: a protocol called WebSub, which everyone still calls PubSubHubbub because that was its name for the first decade.
Google runs a public hub for it. You tell the hub which channel you care about and where to reach you. When that channel publishes, the hub POSTs to your URL. No API key, no Google Cloud project, no quota.
Subscribing is one form post.
curl -X POST https://pubsubhubbub.appspot.com/subscribe \
-d "hub.mode=subscribe" \
-d "hub.topic=https://www.youtube.com/xml/feeds/videos.xml?channel_id=UCxxxxxxxxxxxxxxxx" \
-d "hub.callback=https://your-app.example.com/youtube-hook" \
-d "hub.verify=async" \
-d "hub.lease_seconds=432000"The hub then calls you back to check you meant it. It sends a GET to your callback with a hub.challenge parameter, and you have to echo that value back as plain text with a 200. Return anything else and the subscription is silently dropped.
app.get('/youtube-hook', (req, res) => {
// Echo the challenge back verbatim, as text/plain.
res.type('text/plain').send(req.query['hub.challenge']);
});After that you get POSTs. The body is a small Atom document — enough to tell you something happened and which video it was.
<feed xmlns="http://www.w3.org/2005/Atom">
<entry>
<id>yt:video:VIDEO_ID</id>
<yt:videoId>VIDEO_ID</yt:videoId>
<yt:channelId>UCxxxxxxxxxxxxxxxx</yt:channelId>
<title>The video title</title>
<published>2026-08-21T14:02:11+00:00</published>
<updated>2026-08-21T14:02:11+00:00</updated>
</entry>
</feed>hub.lease_seconds at 432,000. When it runs out the notifications just stop. Nothing errors, nothing logs, and your integration looks fine right up until someone notices it has been quiet since Tuesday. You need a scheduled job that re-subscribes ahead of expiry, and you need it to be more reliable than the thing it is renewing.Every YouTube channel publishes an RSS feed with its most recent uploads, and reading it costs nothing and needs no key:
https://www.youtube.com/feeds/videos.xml?channel_id=UCxxxxxxxxxxxxxxxxPolling is easier to reason about than WebSub. No callback, no handshake, no leases. You trade latency for simplicity, and for most jobs a few minutes of delay doesn't matter.
It has its own edges. YouTube rate-limits the feed and returns a 404 rather than a 429 when it wants you to slow down, which reads like the channel disappeared. It gets stricter overnight. And you're still on your own for the transcript.
| WebSub yourself | Poll the RSS feed | VidProxy | |
|---|---|---|---|
| Public callback needed | Yes | No | No |
| Renewals to maintain | Every 5 days | None | None |
| Duplicate handling | Yours | Yours | Handled |
| Transcript included | No | No | Yes |
| Works from a data centre | Notification yes, captions no | Feed yes, captions no | Yes |
| YouTube Data API quota used | None | None | None |
VidProxy does the watching and hands you the part you actually wanted. You register a channel and a URL. When that channel publishes, we POST you the video and its full transcript in one request.
The captions are fetched through residential connections rather than a data centre, which is the difference between getting a transcript and getting an empty response. There's no subscription to renew, and repeat notifications for the same video are filtered before they reach you.
{
"channel": { "id": "UCxxxxxxxxxxxxxxxx", "name": "Channel name" },
"video": {
"id": "VIDEO_ID",
"title": "The video title",
"url": "https://www.youtube.com/watch?v=VIDEO_ID",
"published": "2026-08-21T14:02:11Z"
},
"transcript": {
"available": true,
"text": "Full transcript text ...",
"segments": [ { "text": "...", "offset": 0.0, "duration": 3.2 } ]
}
}No. It's request-response only, and polling it for new uploads is what drains the quota. Push comes from WebSub instead, which is separate, free, and doesn't count against your Data API units.
Yes. It was renamed WebSub when it became a W3C recommendation, and both names describe the same thing. Google still runs the hub and YouTube still publishes to it. The old name survives mostly because that's what people search for.
Within five days, every time. The hub caps a lease at 432,000 seconds, and when it lapses the notifications stop without telling you.
Not from the hub. You get the video ID, title, channel and timestamps, and fetching captions is a separate problem you inherit.
Because edits republish the entry. A title change fires a fresh notification carrying the video ID you already handled, so deduplicate on that ID.
Only through a tunnel, since the hub has to reach your callback from the public internet. And a genuine end-to-end test still needs a real channel to upload a real video.