How to Download a Screenshot API Response as a File in Python
Save screenshot API output safely in Python: check the response type and status, then write image bytes to disk or follow a URL returned as JSON.
To download a screenshot API response as an image in Python, check that the request succeeded, then write the response body as bytes to a file opened with wb. First confirm the API’s response shape: it may return image bytes directly, redirect to an image, or return JSON containing a URL to download separately.
1. Save a direct image response with Requests
This runnable example uses ScreenshotNeo’s documented GET endpoint. It requests WebP explicitly, checks the HTTP status, verifies the response type, and writes the binary payload. Replace the placeholder with an API key from your account. See the ScreenshotNeo API documentation for request parameters and output options.
import os
from pathlib import Path
import requests
api_key = os.environ["SCREENSHOTNEO_API_KEY"]
response = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": api_key,
"url": "https://stripe.com",
"format": "webp",
},
timeout=90,
)
response.raise_for_status()
content_type = response.headers.get("Content-Type", "").lower()
if not content_type.startswith("image/"):
raise ValueError(
f"Expected an image response, got Content-Type={content_type!r}; "
f"body starts with {response.content[:200]!r}"
)
Path("shot.webp").write_bytes(response.content)
print("Saved shot.webp")
Install the dependency with python -m pip install requests. Set the key in your environment before running:
export SCREENSHOTNEO_API_KEY="YOUR_API_KEY"
python save_shot.py
The important parts are raise_for_status(), binary output, and an extension that matches the format. Requests exposes response content as bytes and response headers separately; a valid JSON parse or a body that was successfully downloaded does not by itself mean the HTTP request succeeded. See the Requests Quickstart and Requests API reference.
2. Identify what the API returned
Do not assume every screenshot endpoint returns image bytes. Inspect the provider documentation and the response’s status, headers, and body format.
| Response shape | What to do | Common mistake |
|---|---|---|
| Raw image bytes | Check status and image content type, then write bytes to a binary file. | Opening the file as text or using a mismatched extension. |
| Redirect to an image | Follow redirects if the API expects it, then save the final response body. Check the final URL and content type. | Saving a redirect response or assuming every client follows redirects the same way. |
| JSON containing a URL | Check status, parse JSON, extract the documented URL field, request that URL, check its status and type, then save its bytes. | Writing JSON text to a file named .png. |
These are different API contracts. For example, some providers return JSON by default and document an option that redirects to the image instead. Follow the specific provider’s instructions; do not copy another provider’s parameter names or response assumptions. See the Screenshot API documentation for an example of provider-specific JSON and redirect behavior.
Save a large response without holding it all in memory
For large full-page captures or PDFs, stream the response to disk in chunks. The temporary file pattern below avoids leaving a seemingly complete destination behind if the download fails partway through.
import os
from pathlib import Path
import requests
api_key = os.environ["SCREENSHOTNEO_API_KEY"]
temporary_path = Path("shot.webp.part")
final_path = Path("shot.webp")
try:
with requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": api_key,
"url": "https://stripe.com",
"format": "webp",
"full_page": "true",
},
stream=True,
timeout=90,
) as response:
response.raise_for_status()
content_type = response.headers.get("Content-Type", "").lower()
if not content_type.startswith("image/"):
preview = next(response.iter_content(chunk_size=512), b"")
raise ValueError(
f"Expected an image response, got {content_type!r}; "
f"body starts with {preview!r}"
)
with temporary_path.open("wb") as output:
for chunk in response.iter_content(chunk_size=64 * 1024):
if chunk:
output.write(chunk)
os.replace(temporary_path, final_path)
except Exception:
temporary_path.unlink(missing_ok=True)
raise
print(f"Saved {final_path}")
stream=True prevents Requests from eagerly loading the complete body. Use iter_content() and skip empty chunks. Requests handles gzip and deflate transfer encodings when iterating this way. Adjust chunk size to suit your workload; it affects buffering, not the screenshot format. The Requests streaming documentation describes this pattern.
When the API returns JSON with a download URL
Use this pattern only when the provider documents a JSON response and a field containing the file URL. The field name below is an example; change it to the documented response schema.
import requests
api_response = requests.get(
"SCREENSHOT_ENDPOINT",
params={"url": "https://example.com"},
timeout=30,
)
api_response.raise_for_status()
payload = api_response.json()
image_url = payload["screenshotUrl"] # Use the provider's documented field.
image_response = requests.get(image_url, stream=True, timeout=30)
image_response.raise_for_status()
content_type = image_response.headers.get("Content-Type", "").lower()
if not content_type.startswith("image/"):
raise ValueError(f"Expected image bytes, got {content_type!r}")
with open("screenshot.png", "wb") as output:
for chunk in image_response.iter_content(chunk_size=64 * 1024):
if chunk:
output.write(chunk)
Do not assume the returned URL is public, permanent, or reusable. The API documentation should say whether it expires, requires authentication, or needs a second request with headers.
3. Use Python’s standard library instead of Requests
urllib.request can make the GET request without adding an HTTP package. This example targets ScreenshotNeo’s GET endpoint and saves the returned bytes. It also checks the media type before creating the output file.
import os
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import urlopen
query = urlencode({
"access_key": os.environ["SCREENSHOTNEO_API_KEY"],
"url": "https://stripe.com",
"format": "webp",
})
endpoint = f"https://api.screenshotneo.com/v1/shot?{query}"
try:
with urlopen(endpoint, timeout=90) as response:
content_type = response.headers.get_content_type()
if not content_type.startswith("image/"):
raise ValueError(f"Expected image bytes, got {content_type!r}")
with open("shot.webp", "wb") as output:
while True:
chunk = response.read(64 * 1024)
if not chunk:
break
output.write(chunk)
except HTTPError as error:
print(f"HTTP error {error.code}: {error.reason}")
raise
except URLError as error:
print(f"Network error: {error.reason}")
raise
Python’s urllib.request documentation covers URL opening and request handling. Requests offers a more convenient interface for status checks, streaming, and headers; the standard library avoids an extra dependency.
4. Format, filenames, and useful request options
- Format and extension: request the format your API supports, then use the corresponding extension, such as
.png,.jpg, or.webp. When the format is not explicit, inspectContent-Typeand provider documentation. Do not infer the format from the URL being captured. - Full page: use the provider’s full-page option when the capture should include content beyond the initial viewport. Lazy-loaded images may need scrolling or a wait strategy supported by that API.
- Timeout: set a finite timeout. Screenshot rendering can take longer than fetching an ordinary static file; choose a value based on the provider’s documented limits and your own job deadline. The examples’ 90 seconds are a code choice, not a universal recommendation.
- Authentication: supply credentials using the method the API documents. Keep keys in environment variables or a secret store; do not commit them or print them in logs. Query-string credentials may appear in proxy or server logs, so use the provider’s supported authentication method and protect request logs.
- Output path: use a deliberate destination and ensure its parent directory exists and is writable. For batch work, generate unique names to avoid overwriting earlier screenshots.
- Redirects: Requests follows ordinary GET redirects by default. If the service returns a URL in JSON, that is not an HTTP redirect: parse the JSON and issue the second request yourself.
5. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. A single GET request returns a PNG, JPEG, WebP, or PDF; use the documented output parameter and a matching filename for the chosen format.
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
See the API docs for the endpoint and options. ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page info, and PDF capture. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan.
Create a free account for 1,000 screenshots a month, with no card required.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The saved file is actually HTML or JSON. | The API returned an error, a challenge page, or a JSON job/result instead of image bytes. | Check the HTTP status before writing; inspect Content-Type and a short body preview. Follow the documented response flow. |
| The image viewer says the file is invalid. | The body was truncated, decoded as text, or saved with an extension that does not match its format. | Write bytes in wb mode, ensure the download completed, and match extension to returned format. |
| The request hangs or raises a timeout. | Rendering took longer than the configured read timeout, or the network stalled. | Set an appropriate finite timeout for the provider’s rendering limits. For long captures, consider its asynchronous job flow if available; do not retry indefinitely. |
| HTTP 401 or 403. | Missing, invalid, expired, or insufficient credentials; sometimes an unsupported authentication method. | Check the key and provider’s authentication instructions. Avoid exposing it in code repositories or logs. |
| HTTP 400 or 422. | A required parameter is missing, misspelled, malformed, or incompatible with another option. | Read the provider’s error body safely and compare parameter names and allowed values with its API reference. |
| HTTP 429. | Rate or concurrency limit reached. | Honor any retry guidance or Retry-After header, reduce concurrency, and use bounded backoff with jitter. |
| HTTP 5xx or connection reset. | Temporary provider or network failure. | Retry only transient failures, with a small bounded retry policy and backoff. Avoid retrying non-idempotent operations unless the API documents safe retry behavior. |
| The saved image is blank or incomplete. | The target page did not finish rendering, requires interaction/authentication, or content loads later. | Check the screenshot service’s page verdict and rendering options. Use documented waits, cookies, headers, or selector readiness when appropriate. |
| Permission denied while saving. | The destination directory is not writable or does not exist. | Choose a writable path and create parent directories before opening the file. |
7. Reliability, performance, and cost
- Keep partial files out of circulation: stream to a temporary path and rename after successful completion. Validate status and response type before finalizing.
- Bound retries: retry transient network errors and selected server errors only, with a maximum attempt count and backoff. Retrying a request also repeats its processing and can increase latency or usage depending on provider billing rules.
- Control memory:
response.contentreads the full body into memory, which is convenient for small captures. Streaming scales better for large files and batches. - Limit concurrency: parallel captures can reduce total wall-clock time but may trigger rate limits or consume available memory and sockets. Follow service limits.
- Check billing semantics: screenshot providers differ in whether failed pages, cache hits, retries, or asynchronous jobs count as usage. Read their pricing and response headers; do not assume a successful HTTP status reveals billability.
- Measure the right thing: no universal screenshot latency or file size applies. Page complexity, full-page height, output format, and network conditions all affect them.
8. FAQ
Should I use response.text to save a PNG?
No. Image data is binary. Use response bytes and a file opened with wb.
Can the API return a PDF instead?
Some screenshot APIs support PDF output. Save the response bytes with a .pdf extension and verify the provider’s content type and documented PDF options.
Is Content-Length required?
No. A response can be streamed without that header. Treat it as useful metadata when present, not proof that the downloaded file is complete.
Can I safely use the captured URL as the output filename?
Usually not directly: URLs may contain characters unsuitable for paths or secrets in query parameters. Prefer a generated identifier or sanitize the name.


