Troubleshooting Website Screenshot APIs
Diagnose blank images, timing bugs, auth failures, selectors, 401s, 429s, 503s, retries, quotas, and reliable screenshot API workflows.

Website screenshot APIs usually fail for a small number of predictable reasons: the response is an error document disguised as an image, the page was captured before JavaScript finished, the request lacked authentication, a selector did not exist, or the client exceeded a rate or usage limit. Start with the HTTP response and structured error fields. Do not change rendering options until you know which class of failure you have.
This guide gives you a repeatable diagnosis for blank or incomplete images, login-protected pages, selector captures, timeouts, 401, 429, 500 and 503 responses, retries, quotas and production reliability. It also shows a complete browser-based workflow and an API option with ScreenshotNeo.
1. Classify the response before changing the capture
First record the status code, content type, response headers and body. A URL ending in .png does not guarantee that the body is a PNG; many APIs return JSON errors with an image-like request path.
curl -i -G "https://api.example.com/screenshot" \
--data-urlencode "url=https://example.com" \
-o response.bin
file response.bin
For a successful image, check for an image content type such as image/png, image/jpeg or image/webp. For an error, parse JSON and inspect fields commonly named error, message, details, a stable error code and a request ID. Screenshot APIs document codes such as unauthorized, invalid_request, rate_limited, quota_exceeded, render_failed and selector_not_found.
| Signal | Likely meaning | First action |
|---|---|---|
| 401 or 403 | Missing, invalid or insufficient credentials | Check the API key, account and required permissions |
| 400 | Malformed URL or unsupported option | Validate the URL and parameter names |
| 404 or selector error | Target route or CSS selector was not found | Open the rendered page and verify the selector |
| 429 | Rate limit exceeded | Honor Retry-After, reduce concurrency and add jitter |
| 500 | Provider or renderer failure | Capture request ID and retry only when the operation is safe |
| 503 | Temporary capacity or upstream failure | Use bounded retries with backoff |
| 200 with blank image | Early capture, blocked content or an empty route | Inspect timing, authentication and page diagnostics |
2. Fix blank and incomplete screenshots
Blank images most often mean that the screenshot happened before client-side rendering completed. Single-page applications can return an empty shell while React, Vue or another framework is still fetching data. A page can also be visually blank because a cookie wall, bot check or login screen replaced the expected content.

Wait for a meaningful condition
Prefer a condition that proves the required content exists. Use a selector wait for a chart, heading or product grid. Use network idle when the page has no continuously polling requests. Add a short post-load delay only when a known animation or late component needs it.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded', timeout: 90000 });
await page.waitForSelector('[data-testid="dashboard-ready"]', { state: 'visible', timeout: 30000 });
await page.waitForTimeout(500);
await page.screenshot({ path: 'dashboard.png', fullPage: true });
await browser.close();
Use networkidle when requests settle. Avoid an indefinite wait: analytics, WebSockets and polling can keep a page busy forever. Browser-rendering services commonly expose networkidle0, networkidle2, waitForSelector and a bounded timeout. Cloudflare’s Browser Rendering guidance notes that JavaScript-heavy pages can otherwise produce empty or incomplete results.
Check the page itself
- Open the exact URL in a normal browser and confirm it renders without a manual click.
- Check whether a consent banner, bot check, redirect or login form is covering the page.
- Confirm the route works without a browser extension, local DNS entry or VPN.
- Inspect the final URL after redirects; the API may have captured a redirect target.
- Check that the viewport is large enough for responsive content to appear.
3. Capture pages that require login
A login page is an authentication problem, not a timing problem. Increasing the delay does not create a session. Supply the authentication method supported by your provider: session cookies, an Authorization header, custom headers or HTTP authentication credentials.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext();
await context.addCookies([{
name: 'session',
value: process.env.SESSION_COOKIE,
domain: 'app.example.com',
path: '/',
httpOnly: true,
secure: true
}]);
const page = await context.newPage();
await page.goto('https://app.example.com/account', { waitUntil: 'networkidle', timeout: 90000 });
await page.waitForSelector('[data-testid="account-page"]', { timeout: 30000 });
await page.screenshot({ path: 'account.png', fullPage: true });
await browser.close();
Never put cookies or API keys in source control or support tickets. Redact them from logs. If the site uses a short-lived token, make sure the capture worker obtains a fresh token before each job. Some providers block private networks, localhost, unsupported ports or destinations that resolve to internal addresses; a valid credential cannot override those URL policies.
4. Diagnose selector and full-page failures
Element screenshots fail when the selector is syntactically wrong, the element is not present in the rendered DOM, it is inside an iframe or shadow root, or it is present but hidden. Verify the selector in browser developer tools after all application code has run.
const target = page.locator('#invoice-summary');
await target.waitFor({ state: 'visible', timeout: 30000 });
await target.screenshot({ path: 'invoice-summary.png' });
For an iframe, switch to its frame before locating the element. For a shadow root, use the browser automation library’s shadow DOM locators. A missing selector should be treated as a request or render error, not retried indefinitely. Full-page captures have their own edge cases: sticky headers may repeat, very tall pages can exceed memory limits, lazy images may remain unloaded and infinite-scroll pages may have no final height.
To improve full-page results, scroll through the document to trigger lazy loading, wait for images to complete, then capture. If the provider offers full-page lazy-image loading, enable it. For extremely long documents, capture sections and stitch them in your application or use a PDF workflow with page ranges.
5. Handle 401, 429, 500 and 503 safely
401 and other credential errors
Check that the key is sent in the required header or query parameter, belongs to the correct account and has not expired. Do not retry unchanged credentials. Confirm the target-site credentials separately from the screenshot-service credentials.
429 rate limits
Read Retry-After when present. Reduce parallel requests, add random jitter and cap the number of attempts. A queue with a fixed worker count is safer than launching one browser per URL.
500 and 503 renderer errors
Retry only transient failures, using exponential backoff such as 1, 2, 4 and 8 seconds with jitter. Stop after a small attempt limit and preserve the request ID. A 503 caused by your concurrency level will continue until concurrency is reduced.
async function withRetry(task, maxAttempts = 4) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await task();
} catch (err) {
const retryable = [429, 500, 503].includes(err.status);
if (!retryable || attempt === maxAttempts) throw err;
const retryAfter = Number(err.retryAfterMs) || 0;
const backoff = Math.min(8000, 1000 * 2 ** (attempt - 1));
const jitter = Math.floor(Math.random() * 250);
await new Promise(resolve => setTimeout(resolve, Math.max(retryAfter, backoff) + jitter));
}
}
}
There is a billing risk when retrying after a client-side timeout: a provider may have completed the capture even though your client stopped waiting. ScreenshotEngine documents this failure mode. Use idempotency keys or provider request-status APIs when available, and check usage before blindly repeating a timed-out job.
6. Timeouts, blocked resources and heavy pages
Separate navigation timeout, selector timeout and overall job timeout. Keep each bounded. Block nonessential fonts, video, advertising and tracking requests when your provider supports resource blocking. This reduces transfer size and prevents third-party failures from holding the page open.
Use domcontentloaded for mostly server-rendered pages. Use a selector or network-idle condition for applications whose visible content arrives after JavaScript executes. Do not use a long delay as a substitute for a readiness signal. A two-minute wait can still capture the wrong state if the app failed to authenticate or returned an error component.
7. A production diagnostic checklist
- Log status, content type, request ID, target URL, method and non-secret options.
- Parse JSON errors before attempting to decode an image.
- Validate that the target is a complete public
httporhttpsURL. - Confirm credentials for both the API and the target website.
- Choose a readiness condition: selector, network idle or a short bounded delay.
- Verify selectors against the rendered DOM and check iframe or shadow-root boundaries.
- Set explicit viewport, timeout and full-page behavior.
- Honor rate-limit and quota headers; cache repeat captures.
- Retry only bounded transient failures, with jitter and a maximum attempt count.
- Escalate with the request ID, status, response body, URL, non-secret parameters and approximate time.
8. Performance, reliability and cost decisions
| Decision | Effect |
|---|---|
| Reuse browser contexts | Reduces startup cost while allowing cookies to be isolated per job |
| Limit concurrency | Prevents local memory pressure and provider 429 responses |
| Cache stable pages | Avoids duplicate work and unnecessary usage |
| Block nonessential resources | Shortens load time and reduces timeout risk |
| Use readiness selectors | Improves correctness compared with arbitrary sleeps |
| Keep retries bounded | Limits duplicate captures and runaway cost |
Compare providers on authentication, selector and full-page support, wait controls, timeout ceilings, resource blocking, private-network policy, error schema, request IDs, rate limits, monthly quotas, caching and retry semantics. Those controls matter more than a screenshot endpoint that only works for a static public page.
9. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP or PDF, while the service handles browser setup and the capture pipeline.

See the ScreenshotNeo documentation for all options. This is a runnable cURL example:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo can wait for a selector, delay or network idle; capture full pages or one CSS-selected element; load lazy images; set cookies, headers, user agents, authorization, timezone and geolocation; click before capture; hide selectors; block ads, trackers, requests or resource types; apply custom CSS and JavaScript; use dark mode, device presets, custom viewports and retina scale; resize images; produce PDFs; cache with a chosen TTL; create signed image links; run asynchronous jobs with signed webhooks; capture up to 100 URLs per bulk call; and expose usage and OpenAPI endpoints.
Cookie banners, newsletter popups and chat widgets are removed before the shot, with each cleanup step configurable. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed. Response headers include X-Page-Verdict and X-Billed, so your worker can distinguish a clean billed capture from a failed or free result. An MCP server provides take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients.
The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is available on every plan. Sign up for the free ScreenshotNeo plan.
10. Short FAQ
Why is the file an image extension but the body is JSON?
The service returned an error document. Inspect status, content type and the JSON error fields before decoding the file.
Should I always use network idle?
No. Use a selector when one element proves readiness. Network idle is useful when requests settle; polling pages may never reach it.
Can a longer timeout fix a login failure?
No. Pass valid cookies, headers or supported HTTP-auth credentials.
Should I retry a selector-not-found error?
Only after correcting the selector, route or readiness condition. Repeating the same request will not create the missing element.
How do I investigate a provider timeout?
Keep the request ID and check whether the provider completed the capture before retrying. A client timeout can occur after a successful, billable capture.
What evidence should support receive?
Send the request ID, status, response body, target URL, method, non-secret parameters and approximate time. Remove keys, cookies and authorization values.


