ScreenshotNeo

BlogHow-to

How to Upload Selenium Screenshots to Discord, Imgur, or Other Services

Capture a Selenium screenshot, keep it in memory or on disk, and upload it safely to Discord, Imgur, or another service.

By the ScreenshotNeo team1 October 20262 min read

Capture and upload are two separate operations. Selenium creates a PNG image file or returns image bytes. Discord, Imgur, and other services then receive that file through their own upload APIs.

The most reliable workflow is:

  1. Open the page and wait for the state you want to share.
  2. Capture the current window, an element, or screenshot bytes.
  3. Check that capture succeeded and that the file is within the destination’s limits.
  4. Upload the file as multipart/form-data or with the destination SDK.
  5. Handle authentication, HTTP errors, and expiry rules for returned URLs.

1. Capture a Selenium screenshot

Selenium’s Python API documents driver.save_screenshot('./image.png') for saving the current window as a PNG. The method returns a Boolean and can fail on an I/O error, so production code should check the result. Selenium also exposes byte and base64 methods when you want to upload without reopening a file. See the Selenium screenshot documentation and Python API reference.

Complete Python capture example

from pathlib import Path
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument("--headless=new")
options.add_argument("--window-size=1440,1000")

driver = webdriver.Chrome(options=options)
try:
    driver.get("https://example.com")
    output = Path("screenshot.png")
    ok = driver.save_screenshot(str(output))
    if not ok:
        raise RuntimeError("Selenium could not save the screenshot")
    print(f"Saved {output} ({output.stat().st_size} bytes)")
finally:
    driver.quit()

Capture bytes instead of a file

png_bytes = driver.get_screenshot_as_png()
base64_png = driver.get_screenshot_as_base64()

Bytes are useful for an HTTP upload because they avoid temporary files. A base64 string is useful only when the destination explicitly accepts base64; do not assume every upload endpoint does.

Capture one element

from selenium.webdriver.common.by import By

element = driver.find_element(By.CSS_SELECTOR, "main article")
element.screenshot("article.png")

An element screenshot limits the shared image to the selected region. Confirm that the selector identifies the intended element after the page has rendered.

Wait for the page state you need

A screenshot call captures the browser’s current state. For dynamic pages, wait for a specific element, a URL change, or a known application condition before capturing. A fixed sleep can be useful as a last resort, but a condition-based wait is usually more stable.

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By

WebDriverWait(driver, 20).until(
    lambda d: d.find_element(By.CSS_SELECTOR, "[data-ready='true']")
)

2. Upload a screenshot to Discord

Discord bot uploads use multipart/form-data. With discord.py, the documented short form is await channel.send(file=discord.File('my_file.png')). The bot must have permission to send messages and attach files in the target channel.

discord.py: save, then send

import asyncio
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import discord

DISCORD_TOKEN = "YOUR_BOT_TOKEN"
CHANNEL_ID = 123456789012345678

class Client(discord.Client):
    async def on_ready(self):
        channel = self.get_channel(CHANNEL_ID)
        if channel is None:
            raise RuntimeError("Channel was not found or is not cached")

        options = Options()
        options.add_argument("--headless=new")
        driver = webdriver.Chrome(options=options)
        try:
            driver.get("https://example.com")
            if not driver.save_screenshot("screenshot.png"):
                raise RuntimeError("Screenshot save failed")
        finally:
            driver.quit()

        await channel.send(file=discord.File("screenshot.png"))
        await self.close()

intents = discord.Intents.default()
asyncio.run(Client(intents=intents).start(DISCORD_TOKEN))

Keep the bot token in an environment variable or secret manager in real deployments. Do not commit it to source control.

Upload with Discord's HTTP API and cURL

Discord's message endpoint accepts a channel ID and a bot authorization header. The exact JSON and multipart formatting matter: payload_json contains the message payload and the file part contains the image.

curl -X POST "https://discord.com/api/v10/channels/CHANNEL_ID/messages" \
  -H "Authorization: Bot YOUR_BOT_TOKEN" \
  -F 'payload_json={"content":"Automated Selenium capture"}' \
  -F 'files[0]=@screenshot.png;type=image/png'

Use the API version and request shape documented by Discord's Create Message reference.

Upload from memory with Python

import requests

with open("screenshot.png", "rb") as image:
    response = requests.post(
        "https://discord.com/api/v10/channels/CHANNEL_ID/messages",
        headers={"Authorization": "Bot YOUR_BOT_TOKEN"},
        files={"files[0]": ("screenshot.png", image, "image/png")},
        data={"payload_json": '{"content":"Selenium capture"}'},
        timeout=30,
    )
response.raise_for_status()
print(response.json())

Upload with Node.js

import fs from "node:fs";

const form = new FormData();
form.append("payload_json", JSON.stringify({ content: "Selenium capture" }));
form.append("files[0]", new Blob([fs.readFileSync("screenshot.png")], { type: "image/png" }), "screenshot.png");

const response = await fetch("https://discord.com/api/v10/channels/CHANNEL_ID/messages", {
  method: "POST",
  headers: { Authorization: "Bot YOUR_BOT_TOKEN" },
  body: form
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);

Embed the uploaded image

To display the attachment inside an embed, use the same filename in the embed URL and the uploaded file:

{
  "embeds": [{ "title": "Selenium result", "image": { "url": "attachment://image.png" } }]
}

Upload the file with the filename image.png. Discord's message documentation describes this attachment reference pattern.

3. Discord size limits and URL lifetime

Limits depend on the endpoint, account, server, and current Discord experiments. Discord's API reference describes a default 20 MiB per-file limit, while the message resource documents a 25 MiB maximum request size. Discord's File Attachments FAQ (updated August 13, 2026) describes 20 MB free uploads, 50 MB Nitro Basic, and up to 500 MB Nitro, while noting that limits are being experimented with. Treat these as context-specific limits and handle a rejected upload rather than assuming one universal number.

Check the screenshot size before sending:

from pathlib import Path
size = Path("screenshot.png").stat().st_size
print(f"{size / 1024 / 1024:.2f} MiB")

Discord attachment CDN URLs are signed and have preset expiry. Discord clients refresh them when appropriate; do not promise that a copied attachment URL is permanent public hosting.

4. Uploading to Imgur

Imgur's surfaced image endpoint reference says an image field can accept binary data, base64 data, or an image URL and lists a 10 MB maximum. That page also says its API documentation moved, and the current authentication, route, and limit were not established in the research for this article. Verify the current Imgur API documentation before shipping an Imgur integration.

The implementation pattern is still the same: keep Selenium's PNG bytes, send them in the field required by the current endpoint, authenticate with the credentials that endpoint requires, and check the response before storing its link. Do not copy Discord's multipart field names, authentication header, size limit, or URL-retention assumptions to Imgur.

5. A reusable upload pipeline for other services

For S3-compatible storage, an internal image service, issue tracker, or chat platform, isolate capture from delivery:

  1. Return bytes and metadata such as filename and MIME type from the capture function.
  2. Make the destination adapter responsible for authentication and request formatting.
  3. Set an explicit timeout and retry only transient failures.
  4. Record the destination response status and request ID without logging secrets.
  5. Delete temporary files after a successful upload when they are no longer needed.
from io import BytesIO
import requests

def upload_multipart(image_bytes: bytes, endpoint: str, token: str) -> dict:
    response = requests.post(
        endpoint,
        headers={"Authorization": f"Bearer {token}"},
        files={"file": ("selenium.png", BytesIO(image_bytes), "image/png")},
        timeout=30,
    )
    response.raise_for_status()
    return response.json()

# image_bytes = driver.get_screenshot_as_png()
# result = upload_multipart(image_bytes, "https://service.example/upload", "TOKEN")

This is a template, not a claim that every service uses a file field or Bearer authentication. Read the destination's current API reference for its field names, MIME types, limits, retention, and authentication.

6. Reliability, performance, and cost

  • Browser startup: Reuse a WebDriver session for batches when isolation requirements permit. Starting Chrome for every image adds latency.
  • Page readiness: Wait for a meaningful selector or application state. Capturing too early produces blank or incomplete images.
  • File size: Use element captures, an appropriate viewport, or image compression when the destination has a strict limit. Keep PNG for sharp text and transparency; use JPEG when photographic content matters more than lossless edges.
  • Retries: Retry connection resets and selected 5xx responses with backoff. Do not blindly retry authentication failures, permission errors, or 4xx size-limit responses.
  • Secrets: Store bot tokens, API keys, and client secrets outside source code and redact them from logs.
  • Cost: Selenium itself runs wherever your browser runs. Destination charges, storage retention, bandwidth, and paid account tiers are service-specific; confirm current terms before selecting a host.

7. Troubleshooting

Symptom Likely cause Fix
save_screenshot returns false Path is unwritable or the browser session is invalid Use an absolute writable path, check disk permissions, and verify the driver is still alive.
Image is blank or incomplete Capture ran before the page finished rendering Wait for a selector or application-ready condition; confirm the URL and browser logs.
Discord returns 401 Invalid, revoked, or incorrectly formatted bot token Use Authorization: Bot TOKEN, rotate the token if exposed, and verify the bot is installed.
Discord returns 403 Bot lacks permission in the channel Grant Send Messages and Attach Files in that channel.
Discord returns 413 or an attachment error File or complete request exceeds the applicable limit Measure the file, reduce dimensions or quality, and check the account and endpoint limit.
Embed image is missing Filename in attachment://... does not match the uploaded filename Use exactly the same filename in both places.
Returned Discord link later fails Attachment CDN URL signature expired Fetch the message through Discord or generate a new link; do not use it as permanent hosting.
Imgur request is rejected Moved documentation, changed authentication, field mismatch, or size limit Check Imgur's current API reference and response body before changing code.

Or skip the browser setup

ScreenshotNeo captures a URL with one request, so you can send the returned image bytes to Discord or another destination without managing Selenium, Chrome, waits, or driver updates. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Before capture, cookie and consent banners, newsletter popups, and chat widgets are removed. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

FAQ

Can Selenium upload directly to Discord?

No. Selenium captures the browser. Use Discord's bot library or HTTP API for the upload.

Should I upload a file or bytes?

Bytes avoid temporary-file cleanup and are convenient for HTTP clients. Files are simpler to inspect and retry manually.

Can I use a Discord attachment URL as permanent hosting?

No. Discord documents signed CDN URLs with preset expiry.

Is the Imgur 10 MB limit guaranteed?

No. The surfaced reference says 10 MB but also says the documentation moved. Verify the current Imgur API before relying on that figure.

How do I send only part of a page?

Locate the target element and call its Selenium screenshot method instead of capturing the whole window.