Guide
Take website screenshots in Python
Playwright for Python launches Chromium and writes a PNG. A hosted screenshot API skips that install and returns image bytes from one HTTP call. This guide shows a complete local script first. The DataTuber example is only the route this website implements, using the Python standard library.
- Published by
- DataTuber
- Published
- Updated
- Reading time
- 10 min read
Key takeaways
- Playwright is the direct approach: install the package, install Chromium, open a page, and call page.screenshot.
- A viewport capture is the window size you set. full_page=True keeps that width and uses the document’s scroll height.
- The HTTP example uses urllib from the standard library. It does not need requests, an SDK, or a browser on your machine.
- On this site, POST /api/screenshots accepts a public https URL, a viewport name, and png or jpg. It does not accept a pixel size or a full-page flag.
- Check the HTTP status before you save the body. A DataTuber error is JSON with a message. A success body is the image.
Ways to capture a page
Two approaches cover most Python screenshot work. Run a browser yourself, or send the URL to a service that runs one for you.
Playwright is the library in the local script. It can open a page on your machine, set a width and height, and capture the full document height. Selenium and other drivers can also do that. This page shows Playwright only.
A hosted screenshot API accepts a page URL and returns an image. Your process does not launch a browser. Authentication, allowed hosts, and viewport fields are defined by that API. Later sections describe only POST /api/screenshots on this DataTuber website. They are not a catalog of other services. The Node.js guide shows the same route from Node.
What to install
Use Python 3. Confirm it with python --version or python3 --version. The HTTP script below uses only modules that ship with Python 3: json, pathlib, and urllib.
- Playwright script: the playwright package, then Playwright’s Chromium build. pip install does not download the browser.
- DataTuber script: no third-party package. A verified account and an API key only if you want the image stored for that workspace.
- Disk and network for the first Chromium download, if you run the local script.
Install Playwright and Chromium
pip install playwright adds the library. playwright install chromium downloads the browser that library launches. Skip the second command and launch fails because the executable is missing. Run both in the same virtual environment.
pip install playwright
playwright install chromium
python screenshot.py
A complete Playwright example
This opens https://example.com at 1280×720, writes viewport.png for that window, writes full-page.png for the full document, then opens the same URL at 390×844 and writes mobile.png. page.close() runs even when navigation raises. browser.close() runs after the three captures. Replace the URL with the page you want.
from playwright.sync_api import sync_playwright
def capture(browser, url, width, height, path, full_page=False):
page = browser.new_page(viewport={"width": width, "height": height})
try:
response = page.goto(url, wait_until="load", timeout=30_000)
if response is None or not response.ok:
status = response.status if response else "no response"
raise RuntimeError(f"Navigation failed ({status}).")
page.screenshot(path=path, type="png", full_page=full_page)
finally:
page.close()
with sync_playwright() as playwright:
browser = playwright.chromium.launch()
try:
capture(browser, "https://example.com", 1280, 720, "viewport.png")
capture(browser, "https://example.com", 1280, 720, "full-page.png", full_page=True)
capture(browser, "https://example.com", 390, 844, "mobile.png")
finally:
browser.close()
wait_until="load" waits for the load event. Pages that draw after that event can still look unfinished. Wait for a selector you care about before calling screenshot if the first image is blank. networkidle waits until network connections settle, and pages that keep a connection open can sit there until timeout.
Viewport and full page
The viewport is the browser window: a width and a height in CSS pixels. page.screenshot() without full_page captures that rectangle only. Content below the fold is not in the file.
full_page=True keeps the viewport width and extends the image to the document’s scroll height. A short page looks almost the same either way. A long article does not.
Full-page capture is a poor fit for a page that grows forever, such as an infinite scroll. Fixed headers can also repeat or cover content in the stitched image. Capture the viewport, or one element with locator.screenshot(), when the full document is not a stable height.
A mobile window
The third call is another viewport, not a device emulator and not a full-page capture. 390×844 is the mobile size this website’s route uses when you send the name mobile. Change the numbers when you need a different window. The script does not set device_scale_factor, so the PNG is one pixel per CSS pixel.
Browser automation and a hosted API
Choose Playwright when the page is on your machine, behind a login you can script, or when you need an exact width, height, and full-page flag. Choose an HTTP API when you do not want to install or operate a browser. The table states what this website’s route accepts. It does not rank products, and it does not report timings. Screenshot API vs. Playwright expands that choice.
Playwright in your Python process compared with POST /api/screenshots on this website. Not a speed or quality ranking.
What you install
- Playwright
- playwright, then Chromium
- POST /api/screenshots
- Nothing beyond Python 3
Who runs the browser
- Playwright
- Your Python process
- POST /api/screenshots
- This website’s capture route
Size
- Playwright
- Any width and height you set
- POST /api/screenshots
- A viewport name, not pixels
Full page
- Playwright
- full_page=True
- POST /api/screenshots
- Not a field on this route
Which URLs
- Playwright
- Any URL the machine can open, including localhost
- POST /api/screenshots
- Public https URLs only
Failure you handle
- Playwright
- A raised navigation or launch error
- POST /api/screenshots
- An HTTP status and a JSON message
The route this site implements
POST https://datatuber.com/api/screenshots is the screenshot route served by this website. Do not send the call to api.datatuber.com. That host is not this API. The body is JSON with url, viewport, and format.
viewport is desktop, tablet, or mobile. In the current presets, desktop is 1280×720, tablet is 768×1024, mobile is 390×844. format is png or jpg. jpeg is accepted and stored as jpeg. Custom pixel sizes, a full-page flag, and a wait_until field are not part of this request.
The URL has to be https, with no username, password, port, or IP address. The server resolves the hostname and rejects localhost, link-local, and private addresses. http URLs are rejected. Screenshots lists the same three fields. Getting started shows the body in other languages.
Desktop and mobile
The script requests desktop, then mobile, and writes desktop.png and mobile.png. Tablet is the same call with viewport set to tablet. Those names are the whole size contract. Sending 390 and 844 as numbers is not a supported body.
Replace YOUR_API_KEY with a key from a verified account if you want the image stored for that workspace. The dashboard shows the secret once. Leave the placeholder in any example you commit. The homepage demo calls the same path without a key and does not store the image. Create a free account from the sign-in page. Opening the email link is what verifies the address.
import json
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
ORIGIN = "https://datatuber.com"
def capture(url, viewport, image_format, path):
body = json.dumps(
{
"url": url,
"viewport": viewport,
"format": image_format,
}
).encode()
request = Request(
ORIGIN + "/api/screenshots",
data=body,
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urlopen(request) as response:
content_type = response.headers.get("Content-Type", "")
if not content_type.startswith("image/"):
raise RuntimeError(
"Expected an image, got " + (content_type or "no content type") + "."
)
Path(path).write_bytes(response.read())
except HTTPError as error:
raw = error.read().decode("utf-8", errors="replace")
try:
payload = json.loads(raw)
except json.JSONDecodeError:
payload = None
message = payload.get("message") if isinstance(payload, dict) else None
if not isinstance(message, str):
message = "HTTP " + str(error.code)
raise RuntimeError(message) from error
except URLError as error:
raise RuntimeError("The request did not connect: " + str(error.reason)) from error
capture("https://www.python.org", "desktop", "png", "desktop.png")
capture("https://www.python.org", "mobile", "png", "mobile.png")
Authentication and the response
Send the key as a Bearer token. urllib raises HTTPError for any status other than success, and the handler reads that body as JSON. When the route recognized the failure, the JSON has a string message. A successful response body is the image, with Content-Type image/png or image/jpeg, and cache-control no-store. The script checks that content type before it writes the file.
Writing the body before you know the call succeeded saves an error as if it were a PNG. The sample in Getting started reads the bytes without that check. Use the script on this page when you need the status handled.
Homepage demo compared with a keyed call on the current DataTuber route. Limits come from the beta plan and rate buckets in the account migration.
Authorization
- Homepage demo
- No Authorization header
- API key
- Bearer token
Path
- Homepage demo
- POST /api/screenshots
- API key
- POST /api/screenshots
Image kept
- Homepage demo
- Not stored
- API key
- Stored for that workspace
Allowance
- Homepage demo
- 5 captures an hour for a hashed IP
- API key
- 100 screenshots in a UTC month, and 6 requests a minute
Shared gap
- Homepage demo
- 11 seconds between captures
- API key
- 11 seconds between captures
Nothing is charged before January 1, 2027. Pricing has no checkout.
Common errors
Playwright: if launch fails because the browser executable is missing, run playwright install chromium in that same environment. If the shell cannot find the playwright command, that environment’s scripts directory is not on PATH. If goto raises a timeout, the load event did not happen within timeout. Increase it, or wait for a specific selector instead of the whole page. A connection error means the URL did not open from that machine. A blank PNG usually means the pixels you wanted were not painted yet.
A full-page file that is enormous, or that never finishes, usually means the page keeps growing. Capture the viewport instead.
This site’s route returns JSON, not an image, when the call is refused. The script raises RuntimeError with the message field. "Send a URL, viewport, and format." means a required field was missing or not one of the accepted names. A network refusal is HTTP 403 with error.code URL_SECURITY_BLOCKED and the message "This URL cannot be captured because it does not meet DataTuber's network rules." The message does not name the blocked address. A content-policy refusal uses CONTENT_POLICY_BLOCKED, a category, and "Screenshot capture is not permitted for this URL under DataTuber's Content Safety Policy." URL_UNDER_REVIEW means no image was captured: "This URL needs a review before a screenshot can be returned. No image was captured." Those categories are not detected automatically. See the Content Safety Policy.
"That API key is not valid." means the bearer token did not match a key. "Verify your email before using DataTuber." means the account cannot create or use a key yet. "Screenshot capture is not configured on this server." means this deployment has no capture credentials. A 429 message asks you to wait. "The capture timed out before an image came back." means the capture did not finish in time. Those strings are the messages implemented in this repository.
What this page is based on
- Playwright
- The script uses Playwright’s Python sync API: chromium.launch, new_page, goto, screenshot, and full_page. No timed run of that script is reported here, and this repository does not pin a Playwright version.
- This website
- POST /api/screenshots, the URL checks, the viewport presets, the image response, and the account migration in this repository. Checked November 6, 2026.
- Not included
- No timed live capture, no performance numbers, and no ranking of other screenshot products. The Python samples were checked for syntax and for the request fields. They were not executed against the live route.
Questions
Do I have to install a browser?
For the Playwright script, yes. pip install playwright is not enough. Run playwright install chromium as well. The urllib script does not install a browser on your machine.
Do I need the requests package?
No. The DataTuber example uses urllib.request, which is in the Python 3 standard library. requests can send the same JSON, but this page does not depend on it.
Can I set a pixel size or a full-page flag on the API?
No. POST /api/screenshots accepts the names desktop, tablet, and mobile, and png or jpg. Playwright accepts a width, a height, and full_page=True. That difference is why the local script and the HTTP script are not interchangeable.
Can Playwright screenshot localhost?
Yes, if the page is reachable from the machine running Python. POST /api/screenshots on this site rejects localhost, IP addresses, and private hosts.
Which host do I call for DataTuber?
https://datatuber.com/api/screenshots. api.datatuber.com is not serving this API. Getting started shows the same body in cURL, JavaScript, Python, and PHP.
Will the DataTuber call charge a card?
No payment is collected before January 1, 2027. Pricing has no checkout. Create a free account verifies an email and lets you issue a key.
Related
- Take a website screenshot in Node.js
The same Playwright and API split, written for Node.js.
- Screenshot API vs. Playwright
When the local browser is the better tool, and when one HTTP call is enough.
- Getting started
The same POST in cURL, JavaScript, Python, and PHP.
- Screenshots
The body fields, and what a keyed call stores.
- Authentication
Email verification, then a bearer key shown once.
- Create a free account
The sign-in page stores an account when you submit an email.