Blog / Entwickler-Guide
So bekommst du eine YouTube-Kanal-ID aus einem Handle (und umgekehrt)
Creator teilen ihren YouTube-Kanal als @handle, aber fast alles in der YouTube Data API läuft über die Kanal-ID. Wenn du nur Handles speicherst, macht eine Umbenennung deine Daten kaputt; wenn du nur IDs speicherst, kannst du Nutzern nicht den Namen zeigen, den sie kennen. Dieser Leitfaden zeigt dir, wie du ein Handle mit einem API-Aufruf in eine Kanal-ID auflöst, wie du von IDs zurück zu Handles kommst, 50 auf einmal, was du mit alten /user/- und /c/-URLs machst und wie du im Standard-Quota bleibst.
Handles, Kanal-IDs und alte Namen
Ein YouTube-Kanal ist über bis zu vier Arten von Kennungen erreichbar. Nur eine davon ist garantiert stabil.
| Kennung | Beispiel | URL-Form | Hinweise |
|---|---|---|---|
| Kanal-ID | UC_x5XG1OV2P6uZZ5FSM9Ttw | youtube.com/channel/UC... | Die ID, mit der YouTube den Kanal identifiziert. Unterscheidet Groß- und Kleinschreibung. Nutze sie als Primärschlüssel. |
| Handle | @GoogleDevelopers | youtube.com/@GoogleDevelopers | Eindeutig, unterscheidet nicht zwischen Groß- und Kleinschreibung, und der Inhaber kann es in YouTube Studio ändern. |
| Alter Nutzername | GoogleDevelopers | youtube.com/user/GoogleDevelopers | Aus dem älteren Nutzernamen-System. Mit dem API-Filter forUsername auflösbar. |
| Alte benutzerdefinierte URL | GoogleDevelopers | youtube.com/c/GoogleDevelopers | Ältere personalisierte URLs, die laut YouTube weiter funktionieren. Kein API-Filter löst sie auf. |
Speichere die Kanal-ID als Schlüssel und das Handle als Anzeigewert, den du aktualisierst. Im Rest des Leitfadens geht es darum, von dem, was der Nutzer dir gegeben hat, zu dieser ID zu kommen.
API-Key einrichten und das Quota verstehen
- Step 1Ein Google-Cloud-Projekt anlegen
Leg in der Google Cloud Console ein Projekt an oder wähle ein bestehendes.
- Step 2YouTube Data API v3 aktivieren
Öffne APIs und Dienste, such YouTube Data API v3 in der Bibliothek und aktiviere sie für das Projekt.
- Step 3Einen API-Key erstellen
Erstelle unter Anmeldedaten einen API-Key. Öffentliche Kanaldaten wie
channels.listnach Handle oder ID brauchen nur einen Key, kein OAuth. Beschränke den Key auf die YouTube Data API. - Step 4Den Key aus dem Code heraushalten
Leg ihn in eine Umgebungsvariable, zum Beispiel
YOUTUBE_API_KEY, und lies ihn zur Laufzeit. Die Beispiele unten machen genau das. - Step 5Dein Quota prüfen
In der Console zeigt die Seite Kontingente der API deine tägliche Zuteilung und den Verbrauch.
Die Zahlen, auf die es ankommt, laut Googles Dokumentation:
- Ein
channels.list-Aufruf kostet 1 Einheit, egal welchen Filter du nutzt. - Projekte bekommen standardmäßig 10.000 Einheiten pro Tag für alle Endpunkte außer
search.listundvideos.insert, die inzwischen eigene Limits von je 100 Aufrufen pro Tag haben. - Jede Anfrage kostet mindestens 1 Einheit, auch ungültige Anfragen.
- Tägliche Quotas werden um Mitternacht pazifischer Zeit zurückgesetzt.
Das Standard-Quota reicht also für etwa 10.000 Handle-Abfragen pro Tag und bis zu 50-mal so viele ID-Abfragen, weil ein Aufruf 50 Kanäle per ID für dieselbe eine Einheit holen kann.
Vom Handle zur Kanal-ID mit forHandle
channels.list akzeptiert genau einen Filter. Der Filter forHandle nimmt ein YouTube-Handle, und laut Referenz darf dem Wert ein @ vorangestellt sein, GoogleDevelopers und @GoogleDevelopers funktionieren also beide. Frag part=id an, wenn du nur die ID brauchst; die Antwort ist kleiner, die Kosten sind gleich.
curl -s "https://www.googleapis.com/youtube/v3/channels?part=id&forHandle=@GoogleDevelopers&key=$YOUTUBE_API_KEY"Das Node-Beispiel unten packt die Anfrage in eine Hilfsfunktion, listChannels, die der Rest des Leitfadens wiederverwendet. Sie liefert eine leere Liste, wenn nichts passt, und wirft bei echten Fehlern wie einem falschen Key oder aufgebrauchtem Quota.
const API = "https://www.googleapis.com/youtube/v3/channels";
async function listChannels(params) {
const key = process.env.YOUTUBE_API_KEY;
if (!key) throw new Error("Set YOUTUBE_API_KEY");
const url = new URL(API);
url.search = new URLSearchParams({ ...params, key }).toString();
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
const data = await res.json();
if (!res.ok) {
const reason = data.error?.errors?.[0]?.reason;
if (reason === "channelNotFound") return [];
throw new Error(`YouTube API ${res.status} ${reason ?? ""}: ${data.error?.message}`);
}
return data.items ?? []; // no match: items is empty or missing
}
/** "@GoogleDevelopers" or "GoogleDevelopers" -> "UC_x5XG1OV2P6uZZ5FSM9Ttw" or null */
export async function channelIdFromHandle(handle) {
const items = await listChannels({ part: "id", forHandle: handle.trim() });
return items[0]?.id ?? null;
}
/** Legacy username from a youtube.com/user/<name> URL -> channel ID or null */
export async function channelIdFromUsername(username) {
const items = await listChannels({ part: "id", forUsername: username.trim() });
return items[0]?.id ?? null;
}
/** Any number of channel IDs -> Map(id -> { title, customUrl }) */
export async function channelsById(ids) {
const unique = [...new Set(ids)];
const found = new Map();
for (let i = 0; i < unique.length; i += 50) {
const batch = unique.slice(i, i + 50);
const items = await listChannels({ part: "snippet", id: batch.join(","), maxResults: "50" });
for (const ch of items) {
found.set(ch.id, { title: ch.snippet.title, customUrl: ch.snippet.customUrl ?? null });
}
}
return found; // IDs missing from the map were not found
}import os
import requests
API = "https://www.googleapis.com/youtube/v3/channels"
def list_channels(**params):
key = os.environ.get("YOUTUBE_API_KEY")
if not key:
raise RuntimeError("Set YOUTUBE_API_KEY")
response = requests.get(API, params={**params, "key": key}, timeout=10)
data = response.json()
if not response.ok:
errors = data.get("error", {}).get("errors") or [{}]
reason = errors[0].get("reason")
if reason == "channelNotFound":
return []
raise RuntimeError(f"YouTube API {response.status_code} {reason}: {data.get('error', {}).get('message')}")
return data.get("items", []) # no match: items is empty or missing
def channel_id_from_handle(handle):
"""'@GoogleDevelopers' or 'GoogleDevelopers' -> 'UC_x5XG1OV2P6uZZ5FSM9Ttw' or None"""
items = list_channels(part="id", forHandle=handle.strip())
return items[0]["id"] if items else None
def channel_id_from_username(username):
"""Legacy username from a youtube.com/user/<name> URL -> channel ID or None"""
items = list_channels(part="id", forUsername=username.strip())
return items[0]["id"] if items else None
def channels_by_id(ids):
"""Any number of channel IDs -> {id: {"title", "customUrl"}}; missing IDs were not found"""
unique = list(dict.fromkeys(ids))
found = {}
for i in range(0, len(unique), 50):
batch = unique[i:i + 50]
for ch in list_channels(part="snippet", id=",".join(batch), maxResults=50):
found[ch["id"]] = {
"title": ch["snippet"]["title"],
"customUrl": ch["snippet"].get("customUrl"),
}
return foundÜbergib Handles so, wie der Nutzer sie geschrieben hat, nachdem du eine eventuelle Prozent-Kodierung aus der URL dekodiert hast. Handles können Buchstaben aus vielen Schriften enthalten, nicht nur ASCII, und sowohl URLSearchParams als auch requests kodieren den Parameter für dich.
Alte Nutzernamen und /c/-URLs
Alte Links in Creator-Bios und Tabellen nutzen oft eine der Formen aus der Zeit vor Handles.
- `youtube.com/user/<name>`: Übergib den Namen mit
channelIdFromUsernamevon oben anforUsername. Das kostet 1 Einheit wie jeder anderechannels.list-Aufruf. - `youtube.com/c/<name>`: Es gibt keinen
channels.list-Filter für benutzerdefinierte URLs. Viele Kanäle haben ein Handle gewählt, das zu ihrem alten Namen passt, ein Versuch mitforHandleund demselben String kann also klappen. Ein Treffer beweist aber nicht, dass es derselbe Kanal ist. Bestätige ihn auf anderem Weg, bevor du dich darauf verlässt, etwa über den Titel oder den Canonical-Link der Seite (unten beschrieben). - `search.list` liefert gerankte Ergebnisse für eine Suchanfrage, keine exakte Abfrage, und das Standardlimit liegt bei 100 Aufrufen pro Tag. Nutze es nicht zum Auflösen von Kennungen.
Von der Kanal-ID zum Handle, 50 auf einmal
Für die Gegenrichtung nutzt du den Filter id, der eine kommagetrennte Liste von Kanal-IDs nimmt. Frag part=snippet an und lies snippet.customUrl. channelsById von oben entfernt doppelte IDs, schickt sie in Paketen zu 50 und liefert eine Map mit der ID als Schlüssel. IDs, die nicht in der Map auftauchen, wurden nicht gefunden, gelöscht oder sind für die API nicht sichtbar.
- Paketgröße. Die Referenz beschreibt
idals kommagetrennte Liste und begrenztmaxResultsauf 50. Ein separates Limit für die Zahl der IDs nennt sie nicht. Der Code schickt deshalb 50 pro Aufruf und setztmaxResults=50, damit der Standardwert 5 nie ein Paket abschneidet. - Reihenfolge. Geh nicht davon aus, dass die Ergebnisse in der Reihenfolge zurückkommen, in der du sie geschickt hast. Indexiere die Antwort nach
id. - `customUrl` ist optional. Die Referenz beschreibt es nur als die benutzerdefinierte URL des Kanals, und ein Revisionshinweis von 2016 sagt, dass nicht alle Kanäle eine haben. Behandle das Feld als nullable, entferne ein führendes
@, falls vorhanden, und prüfe das Format an ein paar Kanälen, die du kennst, bevor du darauf aufbaust. - Titel sind keine Handles.
snippet.titleist der Anzeigename. Er ist nicht eindeutig und kann Leerzeichen und Emojis enthalten.
Nicht gefunden, Fehler und Caching
Eine Abfrage ohne Treffer ist kein Fehler. Bei forHandle und forUsername hat die Antwort einfach keine Einträge, prüfe also auf ein leeres oder fehlendes items-Array. Die dokumentierten Fehler für channels.list sind diese:
| Fehlergrund | HTTP | Wann er auftritt | Was du tust |
|---|---|---|---|
Keine items in einer 200-Antwort | 200 | Handle oder Nutzername existiert nicht | null zurückgeben; den Fehltreffer kurz cachen |
channelNotFound | 404 | Ein Kanal im Parameter id wird nicht gefunden | Als nicht gefunden behandeln, wie die Hilfsfunktionen oben |
invalidCriteria | 400 | Mehr als ein Filter in der Anfrage | Genau einen von forHandle, forUsername oder id schicken |
channelForbidden | 403 | Der Kanal unterstützt die Anfrage nicht, oder sie ist nicht richtig autorisiert | Protokollieren und den Kanal überspringen |
quotaExceeded | 403 | Dein Projekt hat sein Tagesquota verbraucht | Stoppen und nach Mitternacht pazifischer Zeit erneut versuchen oder mehr Quota beantragen |
badRequest (API key not valid) | 400 | Fehlender oder falscher Key | Konfiguration korrigieren; nicht erneut versuchen |
Cache Ergebnisse, um Quota und Latenz zu sparen, aber halte dich an die Regeln von YouTube zum Speichern von Daten. Die YouTube API Services Developer Policies (Abschnitt III.E.4) erlauben, nicht autorisierte API-Daten höchstens 30 Kalendertage zu speichern; danach musst du sie löschen oder aktualisieren. Ein Cache mit einer TTL deutlich unter 30 Tagen, mit dem kleingeschriebenen Handle als Schlüssel, bringt dir den Großteil des Nutzens:
import { channelIdFromHandle } from "./youtube.mjs";
const TTL_MS = 7 * 24 * 60 * 60 * 1000; // refresh well inside YouTube's 30-day limit
const cache = new Map(); // lowercase handle -> { id, at }
export async function cachedChannelId(handle) {
const key = handle.trim().replace(/^@/, "").toLowerCase();
const hit = cache.get(key);
if (hit && Date.now() - hit.at < TTL_MS) return hit.id;
const id = await channelIdFromHandle(handle);
cache.set(key, { id, at: Date.now() }); // caches misses (null) too
return id;
}In Produktion legst du dieselbe Logik in Redis oder eine Datenbanktabelle mit einer Spalte expires_at und gibst Fehltreffern eine kürzere TTL als Treffern, damit ein neu angelegtes Handle schnell erkannt wird.
Der Weg ohne API-Key: die Kanalseite lesen
Öffentliche Kanalseiten enthalten die Kanal-ID in ihrem HTML. Eine Anfrage an https://www.youtube.com/@GoogleDevelopers liefert derzeit eine Seite, deren <link rel="canonical"> auf https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw zeigt. Dasselbe gilt für /c/- und /user/-Seiten. Damit ist das der einzige Weg, einen /c/-Namen ohne Raten aufzulösen.
// Unofficial fallback: reads the public channel page. Fragile, prefer the API.
export async function channelIdFromPage(handle) {
const url = `https://www.youtube.com/@${encodeURIComponent(handle.replace(/^@/, ""))}`;
const res = await fetch(url, {
headers: { "Accept-Language": "en-US" },
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) return null;
const html = await res.text();
const match = html.match(/<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/(UC[A-Za-z0-9_-]{22})"/);
return match?.[1] ?? null;
}import re
from urllib.parse import quote
import requests
CANONICAL = re.compile(r'<link rel="canonical" href="https://www\.youtube\.com/channel/(UC[A-Za-z0-9_-]{22})"')
def channel_id_from_page(handle):
"""Unofficial fallback: reads the public channel page. Fragile, prefer the API."""
url = "https://www.youtube.com/@" + quote(handle.lstrip("@"), safe="._-")
response = requests.get(url, headers={"Accept-Language": "en-US"}, timeout=10)
if not response.ok:
return None
match = CANONICAL.search(response.text)
return match.group(1) if match else NoneNutze das sparsam und nur dort, wo die API nicht hilft. Es ist inoffiziell und fragil:
- Das Markup ist keine dokumentierte Schnittstelle und kann sich jederzeit ändern.
- YouTube kann automatisierten Anfragen Einwilligungsseiten, Rate-Limits oder ein CAPTCHA ausliefern, besonders aus Cloud-IP-Bereichen. In unseren Tests endete das Folgen eines
youtu.be-Links von einem Cloud-Server auf einer CAPTCHA-Seite von Google. - Automatisierter Zugriff auf die Website kann gegen die Nutzungsbedingungen von YouTube verstoßen. Lies sie und nutze lieber die offizielle API.
Die Ansätze im Vergleich
| Ansatz | Eingabe | Kosten | Vorteile | Nachteile |
|---|---|---|---|---|
channels.list mit forHandle | Handle, mit oder ohne @ | 1 Einheit pro Handle | Offiziell, exakter Treffer, funktioniert für jedes Handle | Ein Handle pro Aufruf |
channels.list mit forUsername | Alter /user/-Name | 1 Einheit pro Name | Offiziell, exakter Treffer | Nur für alte Nutzernamen |
channels.list mit id | Bis zu 50 Kanal-IDs | 1 Einheit pro 50 IDs | Offiziell, gebündelt, liefert Titel und customUrl | customUrl kann fehlen |
search.list | Freitext | Eigenes Limit, standardmäßig 100 Aufrufe pro Tag | Findet Kanäle nach Namen | Gerankte Vermutungen, nicht exakt; kleines Kontingent |
| Canonical-Link der Kanalseite | Jede Kanal-URL, auch /c/ | Kein API-Quota | Funktioniert ohne Key; löst /c/-Namen auf | Inoffiziell, fragil, kann blockiert werden, kann gegen die Bedingungen verstoßen |
Für die meisten Produkte ist das richtige Setup einfach: Handles mit forHandle auflösen, die Kanal-ID speichern, Handles in Paketen zu 50 mit dem Filter id aktualisieren und den Seiten-Fallback hinter einem Flag für den seltenen /c/-Link halten. Wenn du auch die Konten des Creators auf anderen Plattformen brauchst: sociallinks.dev ist eine API, die eine Profil-URL von Instagram, TikTok, YouTube oder X nimmt und die Konten des Creators auf anderen Plattformen liefert, mit einem Confidence-Score und dem Beleg für jeden Treffer.
FAQ
Brauche ich das @, wenn ich ein Handle an forHandle übergebe?
Nein. Laut Referenz zu channels.list darf dem Wert von forHandle ein @ vorangestellt sein, und sie nennt sowohl GoogleDevelopers als auch @GoogleDevelopers als Beispiele.
Wie viel Quota verbraucht eine Handle-Abfrage?
Eine Einheit. Jeder channels.list-Aufruf kostet 1 Einheit, und die Standardzuteilung liegt bei 10.000 Einheiten pro Tag, zurückgesetzt um Mitternacht pazifischer Zeit. Auch ungültige Anfragen kosten mindestens 1 Einheit.
Kann ich mehrere Handles in einem Aufruf abfragen?
Nein. forHandle nimmt ein einzelnes Handle. Nur der Filter id akzeptiert eine kommagetrennte Liste. Bündle also die Gegenrichtung (IDs zu Handles) und cache die Hinrichtung.
Ändert sich eine Kanal-ID jemals?
Die Kanal-ID ist die Kennung, mit der YouTube den Kanal eindeutig identifiziert. Eine Änderung von Handle oder Titel ändert sie nicht. Deshalb ist sie der richtige Primärschlüssel. Handles, Titel und benutzerdefinierte URLs können sich alle ändern.
Warum ist snippet.customUrl leer?
Nicht jeder Kanal hat eine benutzerdefinierte URL, wie YouTubes eigene Revisionshistorie anmerkt. Behandle das Feld als optional und zeig ersatzweise die /channel/UC...-URL.
Wie lange darf ich die Ergebnisse cachen?
Die Developer Policies von YouTube begrenzen gespeicherte API-Daten auf 30 Kalendertage, bevor du sie löschen oder aktualisieren musst. Eine TTL von einer Woche oder weniger für Treffer und kürzer für Fehltreffer hält dich deutlich darunter.