Get a Webhook When a
YouTube Channel Uploads

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.

YouTube uses WebSub, not the Data API

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.

Subscribe to a channel's feed
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.

The verification handshake — Express
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.

What arrives when a video is published
<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>

Five things that make this harder than it looks

  1. Your callback has to be public. The hub calls you, which rules out localhost and anything behind a VPN. During development that means a tunnel. In production it means a URL that stays reachable, because a callback that 500s for long enough gets dropped.
  2. Leases expire after five days. Google's hub caps 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.
  3. The same video arrives more than once. The feed republishes on edits, not just on publish. Someone fixes a typo in a title and you get another notification with the same video ID. Every handler needs to remember what it has already seen.
  4. There's no transcript in the payload. You get an ID and a title. Fetching the actual captions is a separate job, and it is the part that fights back — YouTube blocks caption requests from data-centre IP ranges, so the same code that works on your laptop returns empty from AWS, Render or Fly.
  5. You can't easily test it. The only way to trigger a real notification is for a real channel to publish a real video. Most people end up building a mock, which tests their mock.
None of this is exotic. It's a few hundred lines and an afternoon. The catch is that it never quite stays done — the renewals, the deduplication and the blocked caption fetches are all things you maintain forever, for a feature that isn't your product.

Or just poll the RSS feed

Every YouTube channel publishes an RSS feed with its most recent uploads, and reading it costs nothing and needs no key:

The public channel feed
https://www.youtube.com/feeds/videos.xml?channel_id=UCxxxxxxxxxxxxxxxx

Polling 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

Same webhook, with the transcript already in it

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.

What VidProxy POSTs to your endpoint
{
  "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 } ]
  }
}
If you only need the notification and you're happy maintaining it, WebSub is genuinely fine, and free. The case for paying starts at the transcript, because that's the piece you can't reliably solve from a server.

Common questions

Does the YouTube Data API have webhooks?

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.

Is PubSubHubbub still supported?

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.

How often do I have to re-subscribe?

Within five days, every time. The hub caps a lease at 432,000 seconds, and when it lapses the notifications stop without telling you.

Will the webhook include the transcript?

Not from the hub. You get the video ID, title, channel and timestamps, and fetching captions is a separate problem you inherit.

Why do I get notified twice for one video?

Because edits republish the entry. A title change fires a fresh notification carrying the video ID you already handled, so deduplicate on that ID.

Can I test this locally?

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.