Documentation

Everything you need to route TubeHook events into your stack or straight into a Discord channel.

Getting started

Three steps from zero to receiving events.

1Subscribe to a channel
Go to Channels and add any YouTube channel by ID, handle (@channelname), or URL. TubeHook registers a WebSub subscription with YouTube on your behalf and keeps it renewed.
2Create an endpoint
Go to Endpoints and add a destination URL. Pick Generic webhook (signed JSON to your HTTPS endpoint) or Discord (paste a Discord channel webhook URL). Filter by event type and scope to specific channel tags. TubeHook generates a unique signing secret per endpoint.
3Receive and verify events
When a subscribed channel publishes or updates a video, TubeHook sends a signed JSON POST to your endpoint. Verify the tubehook-signature header before processing. Failed deliveries are retried automatically.

Destination types

Each endpoint has a destination type that controls the outbound payload shape. Pick one when you create the endpoint; switch any time.

Generic webhook (JSON)
Default. Signed JSON POST to your HTTPS endpoint. Use for app backends, automation platforms, queues, and agent workflows.

Body is the payload documented under Payload format. Verify the tubehook-signature header before processing — see Webhook security.

Discord
Posts to a Discord channel webhook URL (https://discord.com/api/webhooks/...). Body is a single Discord { content } message. The YouTube link auto-embeds with thumbnail and title.

Setup: in Discord, open Channel Settings → Integrations → Webhooks → New Webhook, copy the URL, paste it into TubeHook with destination type set to Discord.

Event types

The type field in every payload is one of the following values.

Event typeLabel
youtube.video.public_publishedPublic video
youtube.video.updatedVideo updated
youtube.video.deletedVideo deleted
youtube.live.scheduledLive scheduled
youtube.live.startedLive started
youtube.live.endedLive ended
youtube.video.scheduled_releaseScheduled release
youtube.video.members_only_detectedMembers-only
youtube.video.detectedDetected
youtube.unknownUnknown

Payload format

Generic endpoints receive this JSON POST body. Fields inside data.video may be null when YouTube does not provide them for that event type. Discord endpoints receive a { "content": "..." } body instead — see Destination types.

Example payload
{
  "id": "jd7abc123def456",
  "type": "youtube.video.public_published",
  "occurredAt": "2026-05-10T14:30:00.000Z",
  "data": {
    "video": {
      "id": "dQw4w9WgXcQ",
      "title": "New video title",
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "thumbnailUrl": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
      "publishedAt": "2026-05-10T14:28:00.000Z",
      "updatedAt": "2026-05-10T14:30:00.000Z",
      "deletedAt": null,
      "scheduledStartTime": null,
      "actualStartTime": null,
      "actualEndTime": null
    },
    "channel": {
      "id": "UCBcRF18a7Qf58cCRy5xuWwQ",
      "title": "Channel Name",
      "handle": "@channelhandle",
      "url": "https://www.youtube.com/@channelhandle",
      "avatarUrl": "https://yt3.ggpht.com/..."
    }
  }
}
Request headers
Sent with every delivery.
HeaderValue
content-typeapplication/json
user-agentTubeHook Webhooks
tubehook-delivery-idUnique ID for this delivery attempt
tubehook-event-idID of the source YouTube event
tubehook-event-typeSame as payload type field
tubehook-signaturesha256=<hex> HMAC signature

Webhook security

Every request includes a tubehook-signatureheader. It is an HMAC SHA-256 of the raw request body, signed with your endpoint's secret, formatted as sha256=<hex>. Always verify it before processing — use a timing-safe comparison to prevent timing attacks.

Discord endpoints do not need signature verification — Discord does not expose the request body to your code. The header is still sent for parity.

Node.js
import { createHmac, timingSafeEqual } from "crypto";

function verifyTubeHookSignature(
  rawBody: string,
  signature: string,
  secret: string,
): boolean {
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}

// Express example
app.post("/webhook", express.text({ type: "application/json" }), (req, res) => {
  const sig = req.headers["tubehook-signature"] as string;
  if (!verifyTubeHookSignature(req.body, sig, process.env.TUBEHOOK_SECRET!)) {
    return res.status(401).send("Invalid signature");
  }
  const event = JSON.parse(req.body);
  // handle event.type ...
  res.sendStatus(200);
});
Python
import hashlib
import hmac

def verify_tubehook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask example
@app.route("/webhook", methods=["POST"])
def webhook():
    sig = request.headers.get("tubehook-signature", "")
    if not verify_tubehook_signature(request.get_data(), sig, TUBEHOOK_SECRET):
        return "Invalid signature", 401
    event = request.get_json(force=True)
    # handle event["type"] ...
    return "", 200

Your signing secret is available in Endpoints → View secret. Rotate it any time — existing consumers must update before the next delivery.