Monitors & webhooks
Watch a channel, playlist or search, and receive each new video as a signed webhook, optionally with its transcript.
A monitor watches a channel or profile, a playlist, or a search, and tells you about every video that appears after you create it. Each check that finds new videos records one event, POSTs it to your webhook with a signature, and keeps it readable from the events endpoint; with transcripts: true the event carries each new video's transcript as well. It is the push version of polling a channel with since_video_id.
What a monitor can watch
| Type | Target | Platforms |
|---|---|---|
channel | A YouTube @handle, UC… id or channel URL, or a profile URL | YouTube, TikTok, and Instagram |
playlist | A playlist URL | YouTube and TikTok |
search | A keyword query; platform picks where to search, youtube by default | YouTube, TikTok, and Instagram |
On YouTube a monitor can be narrower. A channel monitor takes tab (videos, shorts or live) to watch the channel's uploads, which is the default, its Shorts or its live streams; a creator who posts only Shorts needs shorts. A search monitor takes duration and captions as the search endpoint does, and sets upload_date to today itself: YouTube no longer sorts search results by date, so without it the first page would be mostly old videos. sort and query are not available on monitors. The monitor lists what it was given under options.
A monitor's type, target, platform and options are fixed; create another monitor to watch something else. Search results and pinned posts keep surfacing old videos, so channel and search monitors pass over anything published more than 24 hours before the monitor was created.
A playlist monitor reports videos newly added to the playlist, however old they are; how playlists are read is below.
Create a monitor
curl https://transcriptfetch.com/api/v2/monitors \ -H "Authorization: Bearer $TRANSCRIPTFETCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"type":"channel","target":"@lexfridman","webhook_url":"https://example.com/hooks/transcriptfetch","interval_minutes":60,"transcripts":true,"name":"Lex Fridman uploads"}'
Creating a monitor lists its target once, free. A channel or playlist that does not exist is refused with 404 not_found; otherwise the target's first page today, or all of a YouTube playlist, is recorded as already seen (baseline_count says how many), so you only hear about videos that appear from now on. The monitor is then checked every interval_minutes: 15, 60, 360 or 1440, default 60. webhook_url, interval_minutes, transcripts and name can all be changed later.
{ "ok": true, "request_id": "req_…", "data": { "kind": "monitor", "id": "mon_m3k1x9qz4vb2p7", "type": "channel", "platform": "youtube", "target": "@lexfridman", "options": { "tab": "videos" }, "playlist_length": null, "name": "Lex Fridman uploads", "status": "active", "interval_minutes": 60, "transcripts": true, "webhook_url": "https://example.com/hooks/transcriptfetch", "next_check_at": "2026-09-25T15:00:12.000Z", "last_checked_at": null, "last_error": null, "created_at": "2026-09-24T09:12:40.000Z", "updated_at": "2026-09-24T09:12:40.000Z", "webhook_secret": "whsec_…", "baseline_count": 30 }, "usage": { "credits_spent": 0, "balance": 250 } }
Store webhook_secret now. The 201 response is the only place it appears, and every delivery is signed with it. There is no way to read it again: if you lose it, delete the monitor and create a new one.
How checks run
- One check reports at most 50 new videos. The rest wait for the next checks; none are dropped.
- Scheduled checks run one per account at a time, so an account's monitors take turns.
- A live stream has no transcript until it ends. With
transcripts: true, an upcoming or ongoing broadcast (alivetab monitor finds them early) is looked for every 30 minutes for up to 7 days, and the caption wait starts when the stream ends.
Playlists
- YouTube playlists are read to their end when the monitor is created (up to 2,000 videos). Each check reads the first page and the page at the remembered end, which the monitor knows by its videos as well as its position, so additions at the top, at the end and in the middle (they move the end) are all found. The one gap: a removal plus an insertion that leave the end in place, away from the first page, can be missed.
- TikTok playlists, and YouTube ones that could not be read to their end, are tracked by their first 30 videos (
playlist_lengthisnull). A YouTube playlist tries the full read again every 24 hours. - Implausible reads are held. A check that finds more than 50 new videos, or more than 10 that make up over 20% of the playlist, records and charges nothing and reads the whole playlist again. The videos are reported only once that read finds them too, 50 per check.
- Bulk additions. A long run appended past an end the monitor has confirmed is reported (after that hold). Past an end it never confirmed (first-page tracking, or an earlier read that stopped short) the run is taken for the rest of the playlist coming into view, and remembered without being reported.
- A read never makes a monitor forget a video it has seen, and a playlist only counts as shorter once two reads agree.
Receive the webhook
Each check that finds new videos sends one POST to webhook_url, with the event as its JSON body:
{ "id": "mev_m3k1xa0b7c8d9e", "type": "monitor.videos", "created_at": "2026-09-25T14:00:09.000Z", "monitor": { "id": "mon_m3k1x9qz4vb2p7", "name": "Lex Fridman uploads", "type": "channel", "platform": "youtube", "target": "@lexfridman" }, "credits_spent": 2, "data": { "videos": [ { "videoId": "dQw4w9WgXcQ", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "title": "Example video", "duration": 212, "channel": "Example Channel", "publishedAt": "2026-09-25T13:00:00Z", "stats": { "plays": 1200 } } ], "transcripts": [ { "video_id": "dQw4w9WgXcQ", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "outcome": "ok", "transcript": { "kind": "transcript", "video_id": "dQw4w9WgXcQ", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "platform": "youtube", "title": "Example video", "channel": "Example Channel", "duration": 212, "language": "en", "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg", "source": "captions", "text": "We're no strangers to love …", "segments": [ { "start": 0, "duration": 3.5, "text": "We're no strangers to love" } ] } } ] } }
type is monitor.videos and data.videos holds the new rows exactly as the listing endpoints return them. With transcripts: true, data.transcripts has one entry per video: ok with the transcript, error with the standard error block, or processing while it is still on its way. A transcript that arrives later is sent as its own monitor.transcript event, whose data.videos_event_id names the event that announced the video.
Every delivery carries these headers:
X-TranscriptFetch-Signature:sha256=and the hex HMAC-SHA256 of the raw body, keyed with the monitor'swebhook_secretX-TranscriptFetch-Event: the event type,monitor.videosormonitor.transcriptX-TranscriptFetch-Delivery: the event id, the same on every retry of that event
Answer with any 2xx within 10 seconds. Anything else (another status, a timeout, a redirect, which is never followed, or an unreachable host) is retried after 1, 5, 30, 120 and 720 minutes, and after 6 attempts the delivery is marked failed. A delivery can arrive more than once, so deduplicate on X-TranscriptFetch-Delivery. Webhooks are optional: every event, delivered or not, stays readable from GET /api/v2/monitors/{monitorId}/events for 30 days, with a delivery block saying where it stands.
Verify the signature
Compute the HMAC over the exact bytes you received, before parsing them, and compare in constant time. The key is the whole secret, whsec_ prefix included.
import { createHmac, timingSafeEqual } from "node:crypto"; // rawBody: the request body exactly as received (a Buffer or string). export function verifySignature(rawBody, signatureHeader, secret) { const expected = Buffer.from( "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex"), ); const received = Buffer.from(signatureHeader ?? ""); return received.length === expected.length && timingSafeEqual(received, expected); }
import hashlib import hmac # raw_body: the request body as bytes, before any JSON parsing. def verify_signature(raw_body: bytes, signature_header: str | None, secret: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected.encode(), (signature_header or "").encode())
Read the raw body the way your framework exposes it: express.raw({ type: "application/json" }) in Express, await req.text() in a Next.js route handler, request.get_data() in Flask, await request.body() in FastAPI. Answer 401 to a delivery whose signature does not match, and parse the JSON only once it does.
Check, pause and delete
POST /api/v2/monitors/{monitorId}/checkruns a check now, billed like a scheduled one, and returns what it found within about 85 seconds. Any event it recorded has had its first webhook attempt, unless time ran short: then that attempt, like any transcript it did not reach, is left to the scheduler. It is the quickest way to test your endpoint.PATCH /api/v2/monitors/{monitorId}changeswebhook_url,interval_minutes,transcriptsorname, or setsstatustopausedoractive. A resumed monitor reports what appeared while it was paused, up to the newest page.DELETE /api/v2/monitors/{monitorId}removes the monitor and its events.GET /api/v2/monitorslists your monitors together with your account's limits.
What monitors cost
A check that finds nothing new is free. A check that finds new videos costs 1 credit, the price of one listing page, however many videos it finds. With transcripts: true, each caption transcript costs 1 credit, and a video without captions is transcribed from its audio at 1 credit per started 5 minutes of audio (minimum 1), billed when the transcript is delivered. Brand-new YouTube uploads usually get automatic captions some minutes after they go live, so YouTube captions are retried every 15 minutes for up to 60 minutes before audio transcription is used.
If the account cannot pay when a check finds videos, nothing is recorded: the videos are not marked seen, the monitor's last_error reads insufficient_credits, and the first check after you top up delivers them. Creating, listing, reading, changing and deleting monitors, and reading their events, are free.
Limits
| Limit | Without a paid plan | On any paid plan |
|---|---|---|
| Monitors per account | 5 | 100 |
| Shortest interval | 60 minutes | 15 minutes |
Creating a monitor past the cap answers 400 monitor_limit_reached, and an interval shorter than the plan allows is refused with 400 invalid_request. Monitors need an API key from TranscriptFetch itself: they are not available through a marketplace subscription such as RapidAPI, whose billing cannot meter scheduled checks. Every field, response and error is in the endpoint reference.