I needed a screenshot API for a side project. The existing options were either too expensive ($29+/mo for basic usage) or too complex (100+ parameters to configure). So I built one.
The Problem
Taking website screenshots sounds simple until you actually try it:
- Headless Chrome is resource-hungry and crashes randomly
- Cookie banners, popups, and "accept cookies" dialogs ruin every capture
- Mobile screenshots need proper viewport emulation, not just resizing
- Full-page captures break on infinite scroll sites
I looked at ScreenshotOne ($29/mo), URLBox ($19/mo), and Screenshotlayer (slow, dated API). None felt right for a developer who just wants curl URL > screenshot.png.
What I Built
GrabShot - a screenshot API that prioritizes simplicity:
curl "https://grabshot.dev/api/screenshot?url=https://stripe.com" \
-H "X-API-Key: your-key" \
--output screenshot.png
That's it. One endpoint. Sensible defaults. You get a PNG back.
The Stack
- Express.js - API server
- Puppeteer - headless Chrome for rendering
- Sharp - image processing (resize, format conversion, watermarks)
- SQLite - user accounts, API keys, usage tracking
- Caddy - reverse proxy with automatic HTTPS
Total infrastructure cost: one $5/mo VPS.
The AI Cleanup Feature
The killer feature turned out to be AI-powered cleanup. Before taking the screenshot, the API:
- Detects cookie consent banners
- Identifies newsletter popups and overlays
- Removes them from the DOM
- Then captures a clean screenshot
This alone saves developers hours of writing custom JavaScript to dismiss popups on every target site.
Device Frames
Screenshots look better in context. GrabShot wraps captures in realistic device frames:
- Browser chrome (with address bar)
- iPhone frames
- MacBook frames
- Minimal frames
It's a small touch but makes a huge difference for marketing pages, portfolios, and documentation.
Lessons Learned
1. Puppeteer Memory Management is Critical
Each screenshot spawns a browser page. Without careful cleanup, memory leaks will kill your server in hours. Key patterns:
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
const buffer = await page.screenshot({ type: 'png' });
return buffer;
} finally {
await page.close(); // ALWAYS close, even on error
}
2. networkidle2 > networkidle0
networkidle0 waits for zero network connections, which never happens on sites with analytics, websockets, or polling. networkidle2 (max 2 connections for 500ms) is almost always what you want.
3. Viewport Size Matters More Than You Think
Default Puppeteer viewport is 800x600. Most websites look terrible at that size. Default to 1280x720 minimum. For "desktop" screenshots, 1440x900 produces the best results.
4. The Free Tier is Your Best Marketing
25 free screenshots/month is enough for developers to test thoroughly. The upgrade to $9/mo for 2,500 screenshots happens naturally when they integrate it into production.
Try It
- Free tool: try.grabshot.dev - paste a URL, get a screenshot
- API docs: grabshot.dev/docs
- GitHub: SDKs for Node.js and Python
25 free screenshots/month, no credit card required.
What screenshot challenges have you faced? I'd love to hear about edge cases I should handle.
Top comments (0)