Discord Screenshot Bot — Add /screenshot to Your Server

Build a Discord bot that screenshots any URL with a slash command. Uses discord.js v14 and the ScreenshotRun API. No Puppeteer needed. Free tier included.

Vitalii Holben Updated

The Discord screenshot bot integration from ScreenshotRun lets you add a /screenshot slash command to any Discord server. A user types the command with a URL, your bot calls the API, and the captured image lands in the channel as an embed. The whole flow takes under five seconds and runs on discord.js v14 with the same fetch pattern used in the Node.js integration, no headless browser on your server.

Discord bots that need to preview links or capture webpages usually spin up Puppeteer behind the scenes. That means a Node.js process managing Chromium instances alongside the bot's WebSocket connection to Discord. Memory spikes when multiple users fire commands at once, and the bot goes offline if Chrome crashes. Offloading the rendering to a screenshot API keeps the bot lightweight and the hosting bill small.

Set up the Discord screenshot bot

You need three things before writing code: a Discord application with a bot token, the ScreenshotRun API key, and Node.js 18 or newer.

Create a Discord application at discord.com/developers. Enable the bot user, copy the token, and invite the bot to your server with the applications.commands and bot scopes. Then open the ScreenshotRun dashboard and copy your API key.

Install the dependencies:

npm init -y
npm install discord.js

discord.js v14 includes everything you need. No extra HTTP library required because Node.js 18+ has built-in fetch.

Register the /screenshot slash command

Discord requires you to register slash commands before users can see them. Create a file called register.js:

const { REST, Routes, SlashCommandBuilder } = require("discord.js");

const commands = [
  new SlashCommandBuilder()
    .setName("screenshot")
    .setDescription("Capture a screenshot of any webpage")
    .addStringOption((opt) =>
      opt.setName("url").setDescription("The URL to capture").setRequired(true)
    )
    .addStringOption((opt) =>
      opt
        .setName("format")
        .setDescription("Image format")
        .addChoices(
          { name: "PNG", value: "png" },
          { name: "JPEG", value: "jpeg" },
          { name: "WebP", value: "webp" }
        )
    )
    .addBooleanOption((opt) =>
      opt.setName("full_page").setDescription("Capture the full scrollable page")
    ),
];

const rest = new REST().setToken(process.env.DISCORD_TOKEN);

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

Run it once with DISCORD_TOKEN=... CLIENT_ID=... node register.js. The command shows up in every server where your bot is installed.

Handle the /screenshot command

Create bot.js with the command handler:

const { Client, GatewayIntentBits, AttachmentBuilder, EmbedBuilder } = require("discord.js");

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

client.on("interactionCreate", async (interaction) => {
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName !== "screenshot") return;

  const url = interaction.options.getString("url", true);
  const format = interaction.options.getString("format") ?? "png";
  const fullPage = interaction.options.getBoolean("full_page") ?? false;

  await interaction.deferReply();

  try {
    const params = new URLSearchParams({
      url,
      format,
      full_page: String(fullPage),
      width: "1280",
      response_type: "image",
    });

    const response = await fetch(
      `https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
      { headers: { Authorization: `Bearer ${API_KEY}` } }
    );

    if (!response.ok) {
      await interaction.editReply(`Capture failed: HTTP ${response.status}`);
      return;
    }

    const buffer = Buffer.from(await response.arrayBuffer());
    const filename = `screenshot.${format}`;

    const attachment = new AttachmentBuilder(buffer, { name: filename });
    const embed = new EmbedBuilder()
      .setTitle("Screenshot")
      .setDescription(url)
      .setImage(`attachment://${filename}`)
      .setColor(0x1a1a2e)
      .setTimestamp();

    await interaction.editReply({ embeds: [embed], files: [attachment] });
  } catch (err) {
    await interaction.editReply(`Error: ${err.message}`);
  }
});

client.login(process.env.DISCORD_TOKEN);

Start the bot with DISCORD_TOKEN=... SCREENSHOTRUN_KEY=... node bot.js. When a user runs /screenshot url:https://github.com, the bot defers the reply (showing "thinking..."), calls the API, and posts the image as a rich embed with the URL as description.

interaction.deferReply() buys you 15 minutes to respond instead of the default 3-second window. Screenshot captures typically finish in 2-4 seconds, but deferring prevents timeout errors if a complex page takes longer.

Auto-preview links posted in a channel

A common Discord bot pattern: someone pastes a URL, the bot automatically screenshots it and posts the preview. This requires the MessageContent intent:

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

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

const URL_REGEX = /https?:\/\/[^\s]+/g;

client.on("messageCreate", async (message) => {
  if (message.author.bot) return;

  const urls = message.content.match(URL_REGEX);
  if (!urls) return;

  const targetUrl = urls[0];

  const params = new URLSearchParams({
    url: targetUrl,
    format: "png",
    width: "1280",
    block_cookies: "true",
    block_ads: "true",
    response_type: "image",
  });

  try {
    const resp = await fetch(
      `https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
      { headers: { Authorization: `Bearer ${process.env.SCREENSHOTRUN_KEY}` } }
    );

    if (!resp.ok) return;

    const buffer = Buffer.from(await resp.arrayBuffer());
    const attachment = new AttachmentBuilder(buffer, { name: "preview.png" });

    await message.reply({ files: [attachment] });
  } catch {
    // Silently skip failed captures
  }
});

The bot picks up the first URL in any message, captures it with cookie banners and ads stripped out, and replies with the image. The block_cookies and block_ads parameters keep the screenshots clean without writing custom dismiss logic for each site.

Scheduled website monitoring in Discord

Post screenshots to a channel on a schedule. Useful for tracking competitor websites, monitoring your own landing pages, or checking that a deploy didn't break anything visible:

const { Client, GatewayIntentBits, AttachmentBuilder, EmbedBuilder } = require("discord.js");

const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const CHANNEL_ID = "YOUR_CHANNEL_ID";
const MONITOR_URLS = [
  "https://your-site.com",
  "https://competitor.com",
];

async function captureAndPost() {
  const channel = await client.channels.fetch(CHANNEL_ID);

  for (const url of MONITOR_URLS) {
    const params = new URLSearchParams({
      url,
      format: "png",
      width: "1440",
      full_page: "true",
      block_cookies: "true",
      response_type: "image",
    });

    const resp = await fetch(
      `https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
      { headers: { Authorization: `Bearer ${process.env.SCREENSHOTRUN_KEY}` } }
    );

    if (!resp.ok) continue;

    const buffer = Buffer.from(await resp.arrayBuffer());
    const attachment = new AttachmentBuilder(buffer, { name: "monitor.png" });
    const embed = new EmbedBuilder()
      .setTitle("Scheduled capture")
      .setDescription(url)
      .setImage("attachment://monitor.png")
      .setTimestamp();

    await channel.send({ embeds: [embed], files: [attachment] });
  }
}

client.once("ready", () => {
  console.log(`Bot online as ${client.user.tag}`);
  captureAndPost();
  setInterval(captureAndPost, 6 * 60 * 60 * 1000); // every 6 hours
});

client.login(process.env.DISCORD_TOKEN);

The bot captures full-page screenshots of each monitored URL every six hours and posts them as embeds. Your team sees visual diffs in the Discord channel without opening each site manually. Combine this with the full-page capture parameter to catch changes below the fold.

Screenshot API parameters for Discord bots

Every parameter goes into the URLSearchParams object before the fetch call. These are the most useful ones for bot commands:

ParameterTypeDefaultWhat it does in a bot context
urlstringrequiredThe page your user wants captured
formatstringpngPNG for quality, JPEG for smaller file size in embeds
widthnumber1280Match Discord's embed width (usually 1280 is fine)
heightnumber720Viewport height before capture
full_pagebooleanfalseGrab the entire scrollable page
devicestringdesktopSwitch to mobile or tablet viewport
dark_modebooleanfalseActivate dark color scheme for sites that support it
selectorstringCrop to one element instead of the full viewport
block_cookiesbooleanfalseRemove cookie popups so the screenshot stays clean
block_adsbooleanfalseStrip ads from the captured page
delaynumber0Wait extra ms for JS-heavy pages to finish loading
cssstringInject styles to hide elements or tweak layout before capture

The API documentation has the complete parameter list with response format details.

Dark mode and mobile screenshots in Discord

Slash command options make it easy to let users pick how the screenshot looks. Add dark_mode and device as optional parameters in your command registration, then pass them through to the API:

// In the slash command handler:
const darkMode = interaction.options.getBoolean("dark_mode") ?? false;
const device = interaction.options.getString("device") ?? "desktop";

const params = new URLSearchParams({
  url,
  format: "png",
  dark_mode: String(darkMode),
  device,
  block_cookies: "true",
  response_type: "image",
});

Users run /screenshot url:https://github.com dark_mode:true device:mobile and get a mobile dark-mode capture posted to the channel. One API call, no extra code paths for different viewport sizes or color schemes.

Error handling for Discord bot commands

Bot commands need graceful error handling because users see the failures directly in the channel:

async function safeCapture(url, format = "png") {
  const params = new URLSearchParams({
    url,
    format,
    block_cookies: "true",
    response_type: "image",
  });

  for (let attempt = 0; attempt < 2; attempt++) {
    const resp = await fetch(
      `https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
      { headers: { Authorization: `Bearer ${API_KEY}` } }
    );

    if (resp.ok) {
      return Buffer.from(await resp.arrayBuffer());
    }

    if (resp.status === 429) {
      const wait = Number(resp.headers.get("Retry-After") ?? "5");
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }

    if (resp.status >= 500) {
      await new Promise((r) => setTimeout(r, 2000));
      continue;
    }

    throw new Error(`API returned ${resp.status}`);
  }

  throw new Error("Screenshot capture timed out after retries");
}

The function retries on rate limits (429) and server errors. When it finally fails, the bot can catch the error and reply with a human-readable message instead of crashing silently.

Screenshot API vs. running Puppeteer in your Discord bot

Many Discord bot tutorials show Puppeteer for screenshots. That works on a VPS where you control the environment. Hosted platforms like Railway, Render, and Fly.io make running Chrome harder.

Puppeteer in the bot processScreenshotRun API
SetupInstall Chrome, configure --no-sandbox, handle zombie processesOne fetch call per capture
Memory300-500 MB per Chrome tab on top of your botBot stays under 50 MB
Concurrent users2-3 captures max before OOM on a 1 GB VPSLimited only by API rate, not your server
HostingNeeds a VPS with Chrome installedRuns on any Node.js host including serverless
Cookie/ad removalWrite and maintain dismiss scriptsPass block_cookies=true
Bot stabilityChrome crash can take down the bot processAPI errors caught, bot stays online
Cost$10-20/mo VPS + your maintenance timeFree tier: 200 captures/month

If your bot already uses Puppeteer for other tasks (form filling, login flows, scraping), keep it. If the bot only needs URL-to-image conversion for preview embeds or monitoring, a fetch call is simpler and more stable.

Popular Discord bot screenshot patterns: website monitoring alerts, rich link previews, and evidence capture for moderation teams.

FAQ#

Register a slash command named /screenshot with discord.js v14. In the command handler, call the ScreenshotRun API with fetch, pass the URL from the user input, and reply with the image bytes wrapped in an AttachmentBuilder and EmbedBuilder.

Yes. Instead of managing a local Chrome instance, call the ScreenshotRun REST API from your bot code. The API renders the page server-side and returns image bytes. Your bot stays lightweight and never loads a browser.

Discord gives you 3 seconds to respond to a slash command. Call interaction.deferReply() before the API request. That extends the window to 15 minutes, plenty of time for even complex page captures.

Listen for the messageCreate event, extract URLs from the message content with a regex, call the screenshot API for the first URL, and reply with the image as an attachment. Add block_cookies and block_ads parameters for cleaner previews.

PNG gives the sharpest output. JPEG produces smaller files that upload faster in busy servers. WebP offers a balance of quality and size. Discord supports all three in embeds.

Yes. Since the bot uses a fetch call instead of Puppeteer, it runs on any Node.js host without special Chrome setup. The bot process stays under 50 MB of RAM.

Written by

Vitalii Holben

Co-founder and CEO of ScreenshotRun. Full-stack developer building screenshot API tools that help developers capture, automate, and monitor web pages at scale. Based in Europe, shipping code since 201...

Screenshot API in a few clicks

Free plan includes 200 screenshots every month. No credit card required.

Sign up → copy your key → make your first call