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
| Factor | DIY (Playwright) | Screenshot API |
|---|---|---|
| Volume | < 1000/month | 1000+/month |
| Infrastructure | You manage | They manage |
| Anti-bot handling | Manual | Built-in |
| Cookie banners | Manual coding | One parameter |
| Cost | Server costs | Per-screenshot |
| Maintenance | Ongoing | None |
Getting Started with ScreenshotOne
Install the SDK:
pip install screenshotoneBasic usage:
from screenshotone import Client, TakeOptionsimport 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, TakeOptionsimport asyncioimport aiohttpfrom 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
# Usageasync 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
Block Cookie Banners
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 captureWait for Selector
options = (TakeOptions.url('https://example.com') .selector('.main-content')) # Wait for elementReading URLs from CSV
import csvimport asynciofrom 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 asyncioimport aiohttpfrom 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 resultsCost 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:
- Use an API for high volume (1000+/month)
- Use async requests with controlled concurrency
- Leverage built-in features (cookie blocking, dark mode)
- Implement retry logic for reliability
- 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.