Build a Slack or Discord Bot That Screenshots Websites on Demand

February 25, 2026 · 9 min read

Your team shares URLs all day long. Landing pages, competitor sites, staging environments, client mockups. Instead of asking everyone to open the link and check it themselves, what if your Slack or Discord bot could just capture a screenshot and post it right in the channel?

In this guide, you'll build exactly that: a chat bot that takes a URL, calls a website screenshot API, and posts the image directly in the conversation. No browser automation, no Puppeteer servers, no infrastructure headaches.

Why Use a Screenshot API Instead of Puppeteer?

You could spin up a headless browser on your bot server. But that means managing Chromium dependencies, handling memory leaks, dealing with timeouts on JavaScript-heavy pages, and scaling when multiple people send URLs at once.

A screenshot API handles all of that. You send a URL, you get back an image. The tradeoffs are clear:

ApproachSetup timeMaintenanceConcurrent requests
Self-hosted PuppeteerHoursHigh (memory, updates, crashes)Limited by RAM
Screenshot APIMinutesNoneUnlimited

For a chat bot that needs to respond quickly and reliably, the API approach wins every time. Let's build it.

Prerequisites

The Core: Capturing a Screenshot

Before wiring up any chat platform, let's nail the screenshot capture. Here's the API call you'll be wrapping:

curl

curl "https://grabshot.dev/api/screenshot?url=https://example.com&width=1280&height=720&format=png" \
  -H "X-API-Key: YOUR_API_KEY" \
  --output screenshot.png

The API returns the raw image bytes. You can also request format=webp for smaller files or format=jpeg for maximum compatibility. For chat bots, PNG is usually the best balance of quality and support.

Node.js helper function

const GRABSHOT_KEY = process.env.GRABSHOT_API_KEY;
const GRABSHOT_URL = 'https://grabshot.dev/api/screenshot';

async function captureScreenshot(url, options = {}) {
  const params = new URLSearchParams({
    url,
    width: options.width || '1280',
    height: options.height || '720',
    format: options.format || 'png',
    full_page: options.fullPage ? 'true' : 'false',
  });

  const response = await fetch(`${GRABSHOT_URL}?${params}`, {
    headers: { 'X-API-Key': GRABSHOT_KEY },
  });

  if (!response.ok) {
    throw new Error(`Screenshot failed: ${response.status} ${response.statusText}`);
  }

  return Buffer.from(await response.arrayBuffer());
}

Python helper function

import os
import requests

GRABSHOT_KEY = os.environ["GRABSHOT_API_KEY"]
GRABSHOT_URL = "https://grabshot.dev/api/screenshot"

def capture_screenshot(url, width=1280, height=720, fmt="png", full_page=False):
    resp = requests.get(
        GRABSHOT_URL,
        params={
            "url": url,
            "width": width,
            "height": height,
            "format": fmt,
            "full_page": str(full_page).lower(),
        },
        headers={"X-API-Key": GRABSHOT_KEY},
    )
    resp.raise_for_status()
    return resp.content  # raw image bytes

Option 1: Slack Bot with Slash Command

We'll create a /screenshot slash command. When someone types /screenshot https://example.com, the bot captures the page and posts the image in the channel.

Setting up the Slack app

  1. Go to api.slack.com/apps and create a new app
  2. Under Slash Commands, create /screenshot pointing to your server's /slack/screenshot endpoint
  3. Under OAuth & Permissions, add scopes: commands, files:write, chat:write
  4. Install the app to your workspace and grab the Bot Token

Node.js Slack bot

import express from 'express';
import { WebClient } from '@slack/web-api';

const app = express();
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);

app.use(express.urlencoded({ extended: true }));

app.post('/slack/screenshot', async (req, res) => {
  const { text: url, channel_id, user_id } = req.body;

  // Acknowledge immediately (Slack requires response within 3 seconds)
  res.json({ response_type: 'in_channel', text: `Taking screenshot of ${url}...` });

  try {
    const imageBuffer = await captureScreenshot(url);

    await slack.filesUploadV2({
      channel_id,
      file: imageBuffer,
      filename: 'screenshot.png',
      title: `Screenshot of ${url}`,
      initial_comment: `<@${user_id}> here's your screenshot:`,
    });
  } catch (err) {
    await slack.chat.postMessage({
      channel: channel_id,
      text: `Failed to capture screenshot: ${err.message}`,
    });
  }
});

app.listen(3100, () => console.log('Slack bot listening on :3100'));

That's it. Under 40 lines for a fully functional screenshot bot. The key detail: we respond to Slack immediately with an acknowledgment, then capture the screenshot asynchronously and upload it as a file.

Option 2: Discord Bot with Slash Command

Discord's interaction model is similar. We'll register a /screenshot command and respond with the captured image.

Node.js Discord bot

import { Client, GatewayIntentBits, SlashCommandBuilder, REST, Routes, AttachmentBuilder }
  from 'discord.js';

const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const TOKEN = process.env.DISCORD_BOT_TOKEN;
const APP_ID = process.env.DISCORD_APP_ID;

// Register the slash command (run once)
async function registerCommands() {
  const rest = new REST().setToken(TOKEN);
  await rest.put(Routes.applicationCommands(APP_ID), {
    body: [
      new SlashCommandBuilder()
        .setName('screenshot')
        .setDescription('Capture a website screenshot')
        .addStringOption(opt =>
          opt.setName('url').setDescription('URL to capture').setRequired(true)
        )
        .addBooleanOption(opt =>
          opt.setName('fullpage').setDescription('Capture full page?')
        )
        .toJSON(),
    ],
  });
}

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

  const url = interaction.options.getString('url');
  const fullPage = interaction.options.getBoolean('fullpage') || false;

  await interaction.deferReply(); // gives us 15 minutes to respond

  try {
    const imageBuffer = await captureScreenshot(url, { fullPage });
    const attachment = new AttachmentBuilder(imageBuffer, { name: 'screenshot.png' });

    await interaction.editReply({
      content: `Screenshot of ${url}`,
      files: [attachment],
    });
  } catch (err) {
    await interaction.editReply({ content: `Failed: ${err.message}` });
  }
});

registerCommands().then(() => client.login(TOKEN));

Discord's deferReply() is the equivalent of Slack's immediate acknowledgment. It tells Discord "I'm working on it," giving you up to 15 minutes to respond with the actual image.

Python Version (Discord)

If your team prefers Python, here's the same bot using discord.py:

import os
import io
import discord
from discord import app_commands

DISCORD_TOKEN = os.environ["DISCORD_BOT_TOKEN"]

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)

@tree.command(name="screenshot", description="Capture a website screenshot")
@app_commands.describe(url="URL to capture", fullpage="Capture the full page?")
async def screenshot_cmd(interaction: discord.Interaction, url: str, fullpage: bool = False):
    await interaction.response.defer()

    try:
        image_bytes = capture_screenshot(url, full_page=fullpage)
        file = discord.File(io.BytesIO(image_bytes), filename="screenshot.png")
        await interaction.followup.send(f"Screenshot of {url}", file=file)
    except Exception as e:
        await interaction.followup.send(f"Failed: {e}")

@client.event
async def on_ready():
    await tree.sync()
    print(f"Bot ready as {client.user}")

client.run(DISCORD_TOKEN)

Advanced: Full-Page Screenshots and Mobile Views

The basic bot captures the viewport. But sometimes your team wants to see the entire page, or check how a site looks on mobile. You can extend the command with options:

# Full page screenshot
curl "https://grabshot.dev/api/screenshot?url=https://example.com&full_page=true&format=png" \
  -H "X-API-Key: YOUR_API_KEY" --output full.png

# Mobile viewport (iPhone 14 Pro dimensions)
curl "https://grabshot.dev/api/screenshot?url=https://example.com&width=393&height=852&format=png" \
  -H "X-API-Key: YOUR_API_KEY" --output mobile.png

You could add --mobile or --full flags to your slash command to trigger these variations. Parse the text after the URL and pass the options to the API.

Use Cases Your Team Will Love

Handling Edge Cases

A few things to watch for when building a production-quality screenshot bot:

Start Building Your Screenshot Bot

GrabShot's free tier gives you 25 screenshots per month. Enough to prototype your bot and test it with your team.

Get Your Free API Key

Deployment Tips

For a Slack bot, you need a publicly accessible URL. You can use:

For Discord bots, you just need the bot process running somewhere (no public URL required for slash commands registered globally). A small VPS or even a Raspberry Pi works fine.

What's Next

Once you have the basic bot working, there are plenty of ways to extend it:

The screenshot API does the heavy lifting. Your bot is just the interface. Build once, use it every day.