How to Take Bulk Screenshots in Python with a Screenshot API

Learn how to automate bulk website screenshots in Python using ScreenshotOne API. Process thousands of URLs efficiently with async requests, rate limiting, and error handling.

Blog post 3 min read

Written by

Dmytro Krasun

Published on

I’ve built screenshot systems with both approaches—managing browsers myself and using APIs. For production workloads with thousands of URLs, an API almost always makes more sense. Let me show you why and how.

When to Use an API vs Playwright

FactorDIY (Playwright)Screenshot API
Volume< 1000/month1000+/month
InfrastructureYou manageThey manage
Anti-bot handlingManualBuilt-in
Cookie bannersManual codingOne parameter
CostServer costsPer-screenshot
MaintenanceOngoingNone

Getting Started with ScreenshotOne

Install the SDK:

Terminal window
pip install screenshotone

Basic usage:

from screenshotone import Client, TakeOptions
import shutil
client = Client('your-access-key', 'your-secret-key')
options = (TakeOptions.url('https://example.com')
.format('png')
.viewport_width(1920)
.viewport_height(1080)
.full_page(True))
image = client.take(options)
with open('screenshot.png', 'wb') as f:
shutil.copyfileobj(image, f)

Bulk Processing with the API

Here’s how to process multiple URLs efficiently:

from screenshotone import Client, TakeOptions
import asyncio
import aiohttp
from pathlib import Path
class BulkScreenshotter:
def __init__(self, access_key, secret_key, output_dir='screenshots'):
self.client = Client(access_key, secret_key)
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True)
def _get_signed_url(self, url):
"""Generate a signed URL for the screenshot."""
options = (TakeOptions.url(url)
.format('png')
.viewport_width(1920)
.viewport_height(1080)
.full_page(True)
.block_cookie_banners(True)
.block_chats(True))
return self.client.generate_take_url(options)
async def _fetch_screenshot(self, session, url, output_path):
"""Fetch a single screenshot."""
signed_url = self._get_signed_url(url)
try:
async with session.get(signed_url) as response:
if response.status == 200:
content = await response.read()
with open(output_path, 'wb') as f:
f.write(content)
return {'url': url, 'status': 'success', 'path': str(output_path)}
else:
return {'url': url, 'status': 'failed', 'error': f'HTTP {response.status}'}
except Exception as e:
return {'url': url, 'status': 'failed', 'error': str(e)}
async def process(self, urls, concurrency=5):
"""Process URLs with controlled concurrency."""
semaphore = asyncio.Semaphore(concurrency)
results = []
async def bounded_fetch(session, url, index):
async with semaphore:
output_path = self.output_dir / f'{index:05d}.png'
return await self._fetch_screenshot(session, url, output_path)
async with aiohttp.ClientSession() as session:
tasks = [
bounded_fetch(session, url, i)
for i, url in enumerate(urls)
]
results = await asyncio.gather(*tasks)
return results
# Usage
async def main():
urls = [
'https://example.com',
'https://github.com',
'https://stackoverflow.com',
]
screenshotter = BulkScreenshotter('your-access-key', 'your-secret-key')
results = await screenshotter.process(urls, concurrency=5)
success = sum(1 for r in results if r['status'] == 'success')
print(f"Completed: {success}/{len(results)} successful")
asyncio.run(main())

Built-in Features That Save Time

options = (TakeOptions.url('https://example.com')
.block_cookie_banners(True))

No more writing CSS selectors to hide consent dialogs.

Block Chat Widgets

options = (TakeOptions.url('https://example.com')
.block_chats(True))

Removes Intercom, Drift, and other chat widgets automatically.

Dark Mode

options = (TakeOptions.url('https://example.com')
.dark_mode(True))

Delay for Dynamic Content

options = (TakeOptions.url('https://example.com')
.delay(3000)) # Wait 3 seconds before capture

Wait for Selector

options = (TakeOptions.url('https://example.com')
.selector('.main-content')) # Wait for element

Reading URLs from CSV

import csv
import asyncio
from pathlib import Path
async def process_csv(csv_path, access_key, secret_key):
urls = []
with open(csv_path, 'r') as f:
reader = csv.DictReader(f)
urls = [row['url'] for row in reader]
screenshotter = BulkScreenshotter(access_key, secret_key)
results = await screenshotter.process(urls, concurrency=10)
# Save results
with open('results.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['url', 'status', 'path', 'error'])
writer.writeheader()
writer.writerows(results)
return results
asyncio.run(process_csv('urls.csv', 'your-access-key', 'your-secret-key'))

Error Handling and Retries

import asyncio
import aiohttp
from tenacity import retry, stop_after_attempt, wait_exponential
class RobustScreenshotter:
def __init__(self, access_key, secret_key):
self.client = Client(access_key, secret_key)
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
async def _fetch_with_retry(self, session, url, output_path):
"""Fetch with automatic retry on failure."""
options = (TakeOptions.url(url)
.format('png')
.full_page(True))
signed_url = self.client.generate_take_url(options)
async with session.get(signed_url) as response:
response.raise_for_status()
content = await response.read()
with open(output_path, 'wb') as f:
f.write(content)
return {'url': url, 'status': 'success'}
async def process(self, urls):
results = []
async with aiohttp.ClientSession() as session:
for i, url in enumerate(urls):
try:
result = await self._fetch_with_retry(session, url, f'screenshots/{i}.png')
results.append(result)
except Exception as e:
results.append({'url': url, 'status': 'failed', 'error': str(e)})
return results

Cost Comparison: API vs DIY

Let’s compare costs for 10,000 screenshots/month:

DIY with Playwright

  • Server: $50-100/month (cloud VM)
  • Maintenance: 4+ hours/month
  • Risk: Browser crashes, memory leaks

Screenshot API

  • ~$50-100/month for 10K screenshots
  • Zero maintenance
  • Built-in reliability

For most teams, the API is cheaper when you factor in engineering time.

When to Use Playwright Instead

Use Playwright directly when:

  • You need offline processing
  • Volume is very low (< 100/month)
  • You need custom browser interactions
  • Data must stay on your servers

Integration with Workflows

Zapier Integration

ScreenshotOne integrates with Zapier for no-code workflows. Trigger screenshots from forms, spreadsheets, or other apps.

Webhook Delivery

For async processing, use webhooks:

options = (TakeOptions.url('https://example.com')
.webhook_url('https://your-server.com/webhook'))

Summary

For bulk screenshots in Python:

  1. Use an API for high volume (1000+/month)
  2. Use async requests with controlled concurrency
  3. Leverage built-in features (cookie blocking, dark mode)
  4. Implement retry logic for reliability
  5. Consider total cost including engineering time

Frequently Asked Questions

If you read the article, but still have questions. Please, check the most frequently asked. And if you still have questions, feel free reach out at support@screenshotone.com.

When should I use a screenshot API instead of Playwright?

Use an API when you need high volume (1000+ screenshots), don't want to manage browser infrastructure, need features like ad blocking or cookie banner removal, or want to avoid anti-bot detection issues.

How to automate taking screenshots of websites?

For small volumes, use Playwright with Python. For large volumes or production systems, use a screenshot API like ScreenshotOne that handles browser management, scaling, and edge cases automatically.

What is the best website screenshot API?

It depends on your needs. ScreenshotOne offers a good balance of features, reliability, and pricing. Consider factors like rate limits, supported features (full page, PDF, etc.), and pricing model.

Read more Screenshot rendering

Interviews, tips, guides, industry best practices, and news.

View all posts

Automate website screenshots

Exhaustive documentation, ready SDKs, no-code tools, and other automation to help you render website screenshots and outsource all the boring work related to that to us.