Slack's built-in URL unfurling shows a title and maybe a thumbnail for links people drop into channels. For most websites that preview is useless — a generic favicon, a truncated meta description, and no visual of what the page actually looks like. Teams end up opening links in a browser, taking manual screenshots, and dragging images back into Slack just to have a conversation about a webpage.
A slack screenshot bot built on ScreenshotRun skips all of that. Paste a URL, get a full rendered slack website screenshot posted in the channel. This screenshot API Slack integration covers three approaches: an incoming webhook (works in five minutes), a Bolt SDK slash command (custom bot with options), and a scheduled pipeline for automated screenshots in a Slack channel.
Send a screenshot to Slack with an incoming webhook
This is the fastest path — you don't need a Slack app, a bot user, or an OAuth flow. Create an incoming webhook URL for your channel, call the ScreenshotRun API, and POST the image URL to Slack. You can trigger this from a cron job, a CI pipeline, or any script that runs on a schedule.
Step 1. In your Slack workspace, go to api.slack.com/apps, create a new app (or use an existing one), and enable Incoming Webhooks under Features. Click Add New Webhook to Workspace and pick the channel where screenshots should land.
Step 2. Copy the webhook URL. It looks like https://hooks.slack.com/services/T.../B.../xxx.
Step 3. Run this script (Node.js, but the same logic works in Python, bash, or any language with HTTP):
// Post a website screenshot to a Slack channel via incoming webhook
const SCREENSHOT_API = 'https://api.screenshotrun.com/v1/screenshots/capture';
const API_KEY = process.env.SCREENSHOTRUN_KEY;
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;
const url = 'https://example.com';
// 1. Capture the screenshot
const params = new URLSearchParams({
url,
format: 'png',
width: '1280',
block_cookies: 'true',
block_ads: 'true',
response_type: 'json'
});
const response = await fetch(`${SCREENSHOT_API}?${params}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const result = await response.json();
const imageUrl = result.data.links.image;
// 2. Post to Slack with Block Kit
await fetch(SLACK_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
blocks: [
{
type: 'section',
text: { type: 'mrkdwn', text: `*Screenshot:* <${url}|${url}>` }
},
{
type: 'image',
image_url: imageUrl,
alt_text: `Screenshot of ${url}`
}
]
})
});
The screenshot appears in your channel as a rich Block Kit message with the URL as a clickable link above the image. That's all it takes to send screenshot to Slack — you skip the bot token, the app review, and the OAuth flow entirely. Incoming webhooks are the right choice when you're pushing screenshots on a schedule or from a CI/CD pipeline where there's no user interaction.
Build a slack screenshot bot with Bolt SDK
For interactive use — someone types /screenshot https://example.com and gets the result in the channel — you need a proper Slack app with a slash command. The Bolt SDK handles the wiring.
Create the Slack app. Go to api.slack.com/apps and create a new app from a manifest. Paste this YAML:
display_information:
name: Screenshot Bot
description: Capture website screenshots in Slack
features:
bot_user:
display_name: ScreenshotBot
always_online: true
slash_commands:
- command: /screenshot
description: Capture a website screenshot
usage_hint: "[url] [--mobile] [--fullpage] [--pdf]"
oauth_config:
scopes:
bot:
- commands
- chat:write
- files:write
settings:
socket_mode_enabled: true
After creating the app, install it to your workspace. Grab the Bot Token (xoxb-...) from OAuth & Permissions and the App-Level Token (xapp-...) from Basic Information > App-Level Tokens (create one with connections:write scope). Socket Mode means your bot connects outbound — no public URL or tunnel needed.
Install dependencies and write the bot:
npm init -y && npm install @slack/bolt
const { App } = require('@slack/bolt');
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
appToken: process.env.SLACK_APP_TOKEN,
socketMode: true
});
const API = 'https://api.screenshotrun.com/v1/screenshots/capture';
const API_KEY = process.env.SCREENSHOTRUN_KEY;
app.command('/screenshot', async ({ command, ack, client }) => {
await ack(); // respond within 3 seconds
// Parse the command text: /screenshot https://example.com --mobile --fullpage
const parts = command.text.trim().split(/\s+/);
const url = parts[0];
const flags = parts.slice(1);
if (!url || !url.startsWith('http')) {
await client.chat.postEphemeral({
channel: command.channel_id,
user: command.user_id,
text: 'Usage: /screenshot https://example.com [--mobile] [--fullpage] [--pdf]'
});
return;
}
// Build API parameters from flags
const params = new URLSearchParams({
url,
format: flags.includes('--pdf') ? 'pdf' : 'png',
width: flags.includes('--mobile') ? '390' : '1280',
device: flags.includes('--mobile') ? 'mobile' : 'desktop',
full_page: flags.includes('--fullpage') ? 'true' : 'false',
block_cookies: 'true',
block_ads: 'true',
response_type: 'json'
});
try {
const res = await fetch(`${API}?${params}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
if (!res.ok) {
const errText = await res.text();
await client.chat.postEphemeral({
channel: command.channel_id,
user: command.user_id,
text: `API error ${res.status}: ${errText}`
});
return;
}
const result = await res.json();
const imageUrl = result.data.links.image;
// Post the screenshot with Block Kit
await client.chat.postMessage({
channel: command.channel_id,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `<@${command.user_id}> captured <${url}|${url}>`
}
},
{
type: 'image',
image_url: imageUrl,
alt_text: `Screenshot of ${url}`
},
{
type: 'context',
elements: [{
type: 'mrkdwn',
text: `${flags.includes('--mobile') ? '📱 Mobile' : '🖥️ Desktop'} · ${flags.includes('--fullpage') ? 'Full page' : 'Viewport'} · ${flags.includes('--pdf') ? 'PDF' : 'PNG'}`
}]
}
]
});
} catch (err) {
await client.chat.postEphemeral({
channel: command.channel_id,
user: command.user_id,
text: `Failed: ${err.message}`
});
}
});
(async () => {
await app.start();
console.log('Screenshot bot is running');
})();
Run the bot with your environment variables set:
SLACK_BOT_TOKEN=xoxb-... SLACK_APP_TOKEN=xapp-... SCREENSHOTRUN_KEY=sr_... node bot.js
Now anyone in the workspace can type /screenshot https://example.com and get a rendered PNG in the channel. This slack slash command screenshot setup supports flags: --mobile for a phone-width capture, --fullpage for a scroll capture, or --pdf for a PDF document. You can extend the flag parser to support --dark for dark mode, --stealth for bot-protected sites, or any other API parameter.
How the slash command handles slow pages
Slack requires slash commands to acknowledge within 3 seconds. The await ack() call at the top of the handler satisfies that requirement immediately. The actual screenshot capture takes 3-8 seconds depending on the target page, but that happens after the acknowledgment. The bot uses chat.postMessage to deliver the result whenever it's ready — Slack doesn't impose a timeout on follow-up messages.
For pages that take longer (heavy SPAs, sites behind Cloudflare challenges), you can use ScreenshotRun's webhook parameter to decouple the capture from the response. Set webhook_url to an endpoint on your bot server, and the API will POST the result when the render completes. Your bot then posts the screenshot to the channel asynchronously.
Scheduled screenshots: monitor pages and post to Slack
Some teams want screenshots delivered on a schedule — a daily snapshot of the company homepage, a weekly capture of competitor pricing pages, or an hourly check on a status dashboard. Pair the incoming webhook approach with a cron job:
// scheduled-screenshots.js — run via cron or setInterval
const API = 'https://api.screenshotrun.com/v1/screenshots/capture';
const API_KEY = process.env.SCREENSHOTRUN_KEY;
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;
const targets = [
{ url: 'https://yoursite.com', label: 'Homepage' },
{ url: 'https://competitor.com/pricing', label: 'Competitor pricing' },
{ url: 'https://status.yourservice.com', label: 'Status page' }
];
for (const target of targets) {
const params = new URLSearchParams({
url: target.url,
format: 'png',
width: '1280',
block_cookies: 'true',
response_type: 'json'
});
try {
const res = await fetch(`${API}?${params}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
if (!res.ok) {
console.error(`Failed ${target.label}: HTTP ${res.status}`);
continue;
}
const result = await res.json();
const imageUrl = result.data.links.image;
await fetch(SLACK_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${target.label}* — <${target.url}|${target.url}>\n_${new Date().toISOString().slice(0, 16)} UTC_`
}
},
{ type: 'image', image_url: imageUrl, alt_text: target.label }
]
})
});
} catch (err) {
console.error(`Error on ${target.label}: ${err.message}`);
}
}
Run it with cron (0 9 * * * node scheduled-screenshots.js for daily at 9 AM) or deploy it as a GitHub Action on a schedule trigger. Each run posts fresh screenshots to the channel with timestamps, so the team sees the current state of every tracked page without opening a browser.
What ScreenshotRun handles for you
Your bot passes a URL to the API. On our side, we spin up an isolated Chrome session, load the page, let JavaScript frameworks and lazy-loaded images finish rendering, grab the viewport, and hand back a hosted image URL. The bot posts it to Slack. You don't run Puppeteer or maintain a Chrome binary. Font packages aren't your problem either.
Screenshots shared in Slack need to be readable at a glance — people scroll fast. That's why the API has built-in cleanup: block_cookies=true clears GDPR banners before capture, block_ads=true strips display ads, block_chats=true hides floating support widgets. Each is a single query parameter, not a custom dismiss script that breaks when a site updates its consent dialog. See the blocking features page for the full list of what gets removed.
Beyond cleanup, the slash command flags in the bot above are just the beginning. The API supports dark mode, element-level cropping (grab just a pricing table or hero banner), geolocation targeting (render the page from a specific country), and scrolling video capture for long pages. The Node.js integration guide covers the full parameter set if you want to go deeper. The free tier covers 200 requests per month with no feature gating — every parameter works on every plan. Paid tiers raise the cap when you need more volume.
Uploading screenshots as Slack files (alternative to image URLs)
The examples above use Block Kit's image block, which displays the screenshot from ScreenshotRun's hosted URL. That works for most cases. But if your workspace has strict data retention policies or you want screenshots stored in Slack's file system (searchable, downloadable, subject to Slack's retention rules), upload the image as a file instead.
Slack deprecated files.upload in March 2025. The current flow uses three steps:
// Upload screenshot as a Slack file (post-deprecation flow)
const imageUrl = await captureScreenshot(url); // returns data.links.image from the API
// 1. Download the image bytes
const imageResponse = await fetch(imageUrl);
const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
// 2. Get an upload URL from Slack
const upload = await client.files.getUploadURLExternal({
filename: `screenshot-${Date.now()}.png`,
length: imageBuffer.length
});
// 3. Upload the bytes to Slack's URL
await fetch(upload.upload_url, {
method: 'POST',
body: imageBuffer
});
// 4. Complete the upload and share to channel
await client.files.completeUploadExternal({
files: [{ id: upload.file_id, title: `Screenshot of ${url}` }],
channel_id: channelId
});
This gives you a native Slack file with a preview, download button, and thread replies. The tradeoff: more code, an extra network hop (download image then re-upload), and the files:write scope on your bot. For most teams, the Block Kit image block is simpler and faster.
Use cases where a slack screenshot bot pays off
The most common setup we see: QA engineers paste staging URLs into a #bugs channel, and the bot captures the page inline. The visual evidence lives in the thread, attached to the conversation, not buried in someone's downloads folder. Bug reports with screenshots get resolved faster because there's nothing to interpret — everyone sees the same broken layout.
After deploys, the bot can automatically capture production pages and post them to #releases. If a CSS regression ships, the evidence shows up before anyone files a ticket. Pair this with ScreenshotRun's certified screenshot feature and each capture gets a SHA-256 hash and timestamp — useful for compliance monitoring or post-incident review.
Competitor tracking works well in a dedicated channel. Drop a URL, the bot renders a clean capture (cookie banners and chat widgets already stripped), and the team discusses the competitor's new pricing layout or landing page redesign without everyone opening the site separately. Schedule the captures weekly and the channel becomes a visual changelog of every competitor you're watching. The brand monitoring use case page covers this pattern in depth.
A smaller but useful pattern: prospect qualification. Someone pastes a lead's website into #leads, and the bot drops a full render right in the thread. The sales team discusses the site without opening twenty tabs — load speed, layout quality, and tech stack are all visible in the screenshot. One less click per lead adds up across a pipeline of hundreds.
Slack vs Discord: different platforms, different patterns
If you've seen the Discord integration, the Slack version follows a similar concept but with different APIs and conventions. Discord bots use discord.js with message embeds and respond directly to gateway events. Slack bots use Bolt SDK with Block Kit messages and authenticate via workspace-level OAuth. Discord bots are typically deployed per-server by community admins. Slack bots are installed workspace-wide by IT admins and subject to enterprise policies around data retention, SSO, and compliance.
The biggest practical difference: Slack's Socket Mode lets your bot run behind a firewall with no public URL — helpful for internal tools at companies that restrict inbound connections. Discord requires either a public HTTP endpoint or a persistent WebSocket connection to Discord's gateway.
From a code perspective, the Slack bot in this guide is about 80 lines. A comparable Discord bot using discord.js is about the same length but uses a different interaction model — Discord slash commands register globally via the REST API, while Slack slash commands register through the app manifest. If your team uses both platforms, the ScreenshotRun API call is identical in both bots — only the message formatting layer changes.
Troubleshooting
If the bot responds but no image appears, check these common issues:
Block Kit image shows "This image cannot be displayed" — Slack needs a publicly accessible URL. ScreenshotRun's JSON response returns a hosted URL that works out of the box, but if you're self-hosting or proxying the image, make sure it's reachable from Slack's servers and returns a valid Content-Type header.
The slash command times out. Slack expects acknowledgment within 3 seconds. Make sure await ack() runs before the API call, not after. The screenshot capture itself can take as long as it needs — just post the result with chat.postMessage when it's done.
Resolution and cropping issues usually come from the width parameter. Slack's message column is about 600px wide, but the underlying image should still be full resolution (1280px+). Slack scales it down for display but preserves the original when clicked. Avoid setting width below 1024px unless you specifically want mobile captures.
Channel permissions trip people up regularly. The bot must be invited to the channel before it can post — mention @ScreenshotBot or use /invite @ScreenshotBot. For private channels, the bot needs explicit membership. Also verify that the chat:write scope is granted in your app's OAuth settings.
Rate limits can affect both APIs in this setup. ScreenshotRun's free tier allows 200 captures per month. If you hit the limit, the API returns a 429 status code — your bot should catch this and post an error message to the channel instead of failing silently. On the Slack side, chat.postMessage has a rate limit of roughly one message per second per channel. Bursting dozens of scheduled screenshots at once will trigger Slack's rate limiter. Space them out with a short delay between posts.
If the screenshot shows a blank page or a loading spinner, the target site probably needs more time to render. Add the delay parameter (in seconds) to give JavaScript-heavy pages time to finish loading. For SPAs built with React, Vue, or Angular, a delay of 2-3 seconds usually does the trick. You can also use wait_for_selector to wait for a specific element to appear before capturing — more reliable than a fixed delay when the load time varies.
FAQ#
Create a Slack app with the Bolt SDK and register a slash command like /screenshot. The bot calls the ScreenshotRun API with the URL, gets back a hosted image link, and posts it to the channel using Block Kit. Socket Mode means the bot runs behind your firewall with no public URL needed.
Yes. Use an incoming webhook — create one in your Slack workspace settings, then call the ScreenshotRun API and POST the image URL to the webhook. It takes about five minutes to set up and works from any script, cron job, or CI pipeline.
Slack's Block Kit image block requires a publicly accessible URL. ScreenshotRun's API returns a hosted image URL that works out of the box. If you're proxying or self-hosting the image, make sure it's reachable from Slack's servers and returns the correct Content-Type header.
Write a short script that calls the ScreenshotRun API for each URL and posts the results to a Slack incoming webhook. Run it with cron (e.g., daily at 9 AM) or as a GitHub Actions workflow on a schedule trigger.
Block Kit image blocks display the screenshot from a hosted URL — simple and fast. Uploading as a Slack file stores the image in Slack's file system, making it searchable and subject to Slack's retention policies. File uploads require the files:write scope and an extra network hop (download from API, then re-upload to Slack).