← Back to blog

Build a Discord Bot That Fetches YouTube Transcripts

Zied · 8/12/2026 · 7 min read

Build a Discord Bot That Fetches YouTube Transcripts

Paste a YouTube link into a Discord channel and get the full transcript back in seconds. That is the entire premise of this bot, and it takes less than 100 lines of Node.js to build. The heavy lifting, extracting captions across 125 languages while dodging YouTube's IP blocks, goes to the YouTube Transcriber API.

This guide walks through registering a slash command, parsing the video ID, calling the transcript API, formatting the response for Discord, and handling the two failure modes that trip up most first implementations.

What You Will Build

A Discord slash command /transcript that accepts a YouTube URL and returns the transcript as a formatted message. If the transcript is longer than Discord's 2,000-character limit, the bot paginates across multiple replies. For videos with captions disabled, it replies with a clear error.

The same pattern powers more sophisticated tools. If you want to add an AI summarization layer on top, the Build a Slack Bot That Summarizes YouTube Videos guide covers the summarization step in detail and translates directly to Discord.

Prerequisites

Install the one dependency:

npm install discord.js

Register the Slash Command

Discord requires you to register slash commands before they appear in the UI. Create a deploy-commands.js file and run it once:

// deploy-commands.js
const { REST, Routes, SlashCommandBuilder } = require('discord.js');

const commands = [
  new SlashCommandBuilder()
    .setName('transcript')
    .setDescription('Fetch the transcript for a YouTube video')
    .addStringOption(option =>
      option
        .setName('url')
        .setDescription('YouTube video URL or ID')
        .setRequired(true)
    )
    .toJSON()
];

const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);

(async () => {
  await rest.put(
    Routes.applicationCommands(process.env.CLIENT_ID),
    { body: commands }
  );
  console.log('Slash command registered.');
})();

Run it with:

DISCORD_TOKEN=your_token CLIENT_ID=your_app_id node deploy-commands.js

Global command registration can take up to an hour to propagate. For faster iteration during development, register to a specific guild by replacing Routes.applicationCommands with Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID).

Parse the Video ID

YouTube URLs come in several shapes. A helper function normalizes them to a plain video ID:

function extractVideoId(input) {
  // Already a bare ID like dQw4w9WgXcQ
  if (/^[a-zA-Z0-9_-]{11}$/.test(input)) return input;

  try {
    const url = new URL(input);
    if (url.hostname === 'youtu.be') return url.pathname.slice(1);
    return url.searchParams.get('v');
  } catch {
    return null;
  }
}

Passing the raw ID or full URL directly to the API also works since the video_url parameter accepts both forms, but normalizing early makes error messages cleaner.

Call the YouTube Transcript API

The transcript endpoint is a single authenticated GET request:

GET https://getyoutubetranscriber.com/api/v2/transcript

Required parameter: video_url. Optional parameters include lang for a specific language code and send_metadata=true to get the video title alongside the transcript.

async function fetchTranscript(videoId, lang = null) {
  const params = new URLSearchParams({
    video_url: videoId,
    send_metadata: 'true',
    include_timestamp: 'false', // omit timestamps for cleaner Discord output
  });

  if (lang) params.set('lang', lang);

  const res = await fetch(
    `https://getyoutubetranscriber.com/api/v2/transcript?${params}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.YT_API_KEY}`,
      },
    }
  );

  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw Object.assign(new Error(err.title || 'API error'), { status: res.status });
  }

  return res.json();
}

A successful response looks like this:

{
  "video_id": "dQw4w9WgXcQ",
  "language": "en",
  "transcript": [
    { "text": "We're no strangers to love", "start": 18640, "duration": 3240 },
    { "text": "You know the rules and so do I", "start": 21880, "duration": 3320 }
  ],
  "metadata": {
    "title": "Rick Astley - Never Gonna Give You Up",
    "author_name": "Rick Astley",
    "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg"
  }
}

Join the text fields into a single string for the Discord reply:

function formatTranscript(data) {
  const title = data.metadata?.title ?? data.video_id;
  const body = data.transcript.map(s => s.text).join(' ');
  return { title, body };
}

Handle Rate Limits and Long Messages

Two failure modes matter here.

Rate limit responses (429)

The API returns 429 when you exceed the request rate. Retry with exponential backoff:

async function fetchWithRetry(videoId, lang, maxAttempts = 3) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fetchTranscript(videoId, lang);
    } catch (err) {
      if (err.status === 429 || err.status === 408) {
        // 408 is a temporary upstream failure and is also safe to retry
        const delay = 1000 * 2 ** attempt;
        await new Promise(r => setTimeout(r, delay));
        continue;
      }
      throw err; // non-retryable, surface immediately
    }
  }
  throw new Error('Max retry attempts reached');
}

For more detail on building a resilient retry loop around the API, see Retry Strategies for a Reliable YouTube Transcript Pipeline.

Message length limits

Discord enforces a 2,000-character cap per message. A transcript for a 30-minute video can run to 20,000 characters or more. Chunk the body before sending:

function chunkText(text, maxLen = 1900) {
  const chunks = [];
  let start = 0;
  while (start < text.length) {
    let end = start + maxLen;
    // Break at the last space before the limit to avoid splitting words
    if (end < text.length) {
      const boundary = text.lastIndexOf(' ', end);
      if (boundary > start) end = boundary;
    }
    chunks.push(text.slice(start, end).trim());
    start = end;
  }
  return chunks;
}

Wire It Together

The full interaction handler defers the reply immediately to avoid Discord's 3-second timeout, then sends the transcript in chunks:

const { Client, GatewayIntentBits } = require('discord.js');

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.on('interactionCreate', async interaction => {
  if (!interaction.isChatInputCommand() || interaction.commandName !== 'transcript') return;

  // Defer publicly so the channel sees the bot is working
  await interaction.deferReply();

  const input = interaction.options.getString('url');
  const videoId = extractVideoId(input);

  if (!videoId) {
    await interaction.editReply('That does not look like a valid YouTube URL or video ID.');
    return;
  }

  try {
    const data = await fetchWithRetry(videoId);
    const { title, body } = formatTranscript(data);
    const chunks = chunkText(body);

    // Send the first chunk as the deferred reply
    await interaction.editReply(`**${title}**\n\n${chunks[0]}`);

    // Follow up with remaining chunks
    for (let i = 1; i < chunks.length; i++) {
      await interaction.followUp({ content: chunks[i] });
    }
  } catch (err) {
    if (err.status === 404) {
      await interaction.editReply('No transcript found for that video. Captions may be disabled.');
    } else if (err.status === 401) {
      await interaction.editReply('API key is missing or invalid.');
    } else {
      await interaction.editReply(`Something went wrong: ${err.message}`);
    }
  }
});

client.login(process.env.DISCORD_TOKEN);

Gotcha: Ephemeral vs Public Replies

Choosing between ephemeral and public replies affects how the bot feels to use.

An ephemeral reply (pass { ephemeral: true } to deferReply) is visible only to the user who invoked the command. This makes sense for raw transcript dumps, which are long and clutters channels. The downside is that follow-up messages from interaction.followUp are also ephemeral by default when the initial reply was ephemeral, so multi-part transcripts stay private.

A public reply is visible to everyone. This works well when you add a summarization step and send a short paragraph rather than the full text. It also lets other members upvote or react to useful finds.

A practical pattern: defer ephemerally, then ask the user whether they want to share the summary publicly:

await interaction.deferReply({ ephemeral: true });
// ... fetch and summarize ...
await interaction.editReply({
  content: summary,
  components: [shareButton] // a button that posts to the channel
});

For an LLM-based summarization layer that feeds into this pattern, the Build a YouTube Video Q&A Chatbot with Transcripts guide shows how to pass transcript text to GPT or Claude and shape the output.

Environment Variables

Keep credentials out of source code:

DISCORD_TOKEN=your_discord_bot_token
CLIENT_ID=your_discord_application_id
YT_API_KEY=your_youtube_transcriber_api_key

Load them with dotenv in development or inject them through your deployment platform in production.

What to Build Next

Once the basic bot works, a few additions make it significantly more useful:

  • Language selection. Add a second slash command option for lang and pass it through to the API. The API supports 125+ language codes, so users can pull French, Spanish, or Japanese captions from the same video.
  • Summarization. Join the transcript, send it to an LLM, and post a 3-5 sentence summary instead of raw text. Discord becomes a lightweight research tool for your community.
  • Channel monitoring. Combine the transcript endpoint with the channel uploads endpoint to post transcripts automatically whenever a subscribed creator publishes a new video.

Discord has 19 million active servers per week according to the platform's own reporting, and developer-built bots are one of the main reasons communities stay engaged. A bot that surfaces video content as searchable text adds real value to any server focused on learning, research, or media.

Get started with 100 free credits at getyoutubetranscriber.com. No credit card required. The docs at getyoutubetranscriber.com/docs cover the full parameter list, error reference, and bulk transcript endpoint if you need to scale beyond single-video lookups.

Build a Discord Bot That Fetches YouTube Transcripts | YouTube Transcriber