selectornotfound
selector_not_found means your CSS selector is valid, but the page had no matching element at the moment of capture. The request depended on that element, so the API returned an error instead of a screenshot. Find out where the element is, or why it is missing, then adjust the selector or the wait.
| Field | Value |
|---|---|
| HTTP status | 422 Unprocessable Entity |
error.type |
target_error |
error.code |
selector_not_found. Some failures arrive today with the generic code screenshot_failed and the message Selector not found: ..., so match on both |
retryable |
true (helps only when the element appears late) |
| Credit used | No |
This code is part of the target errors in the error code reference. A selector that is not valid CSS gives invalid_selector instead.
Example response
{ "error": { "type": "target_error", "code": "selector_not_found", "message": "Selector not found: .pricing-table", "retryable": true }, "request_id": "req_93d5a1e7c08f4b26" }
Which parameters can cause it
capture.selectorwithcapture.mode: "element", whencapture.selector_requiredistrue. See element screenshots.page.clickwhenpage.click_requiredistrue. The message then starts withClick selector not found.
Common causes
| Cause | How to recognize it | Fix |
|---|---|---|
| The page adds the element with JavaScript after load | Element missing from curl output |
Wait for it: wait.until: "networkidle" or wait.for_selector (see wait strategies) |
| The site changed its markup | The selector worked last month | Use a stable hook such as an id or data-* attribute |
| The element is inside a shadow root | Found by the script below as inShadow |
Set page.shadow_dom: true, or capture its host element |
| The element is inside an iframe | Found as inFrames |
Capture the iframe's own URL instead |
| The layout differs at 1280x800 or on mobile | Element exists only at some widths | Set viewport.width or a device to match your test |
| The renderer got another page | Consent wall, login page or bot challenge | Capture the viewport once to see what loaded; see the bot protection guide |
A real example of the first two causes: https://example.com has no h1 element today, although older tutorials use h1 on that page. Its icon is an svg element that a script adds. curl shows zero svg elements, and the browser shows one.
Find out where the element is
This script loads the page like a default capture (1280x800, wait for network idle). It then reports matches in the main document, in open shadow roots and in iframes, and whether the first match has a size:
// find-selector.mjs (Node 22, puppeteer) // Usage: node find-selector.mjs <url> <css-selector> import puppeteer from 'puppeteer'; const [url, selector] = process.argv.slice(2); if (!url || !selector) { console.error('Usage: node find-selector.mjs <url> <css-selector>'); process.exit(2); } const browser = await puppeteer.launch(); try { const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); // the API's default viewport await page.goto(url, { waitUntil: 'networkidle0', timeout: 30_000 }); const report = await page.evaluate((sel) => { const nodes = [...document.querySelectorAll(sel)]; // Look inside open shadow roots, which document.querySelectorAll does not enter. let inShadow = 0; const walk = (root) => { for (const el of root.querySelectorAll('*')) { if (el.shadowRoot) { inShadow += el.shadowRoot.querySelectorAll(sel).length; walk(el.shadowRoot); } } }; walk(document); const first = nodes[0]; const box = first?.getBoundingClientRect(); return { matches: nodes.length, inShadow, firstVisible: first ? box.width > 0 && box.height > 0 : null, firstBox: first ? { x: box.x, y: box.y, width: box.width, height: box.height } : null, }; }, selector); // Same-origin and cross-origin iframes are separate documents. let inFrames = 0; for (const frame of page.frames()) { if (frame === page.mainFrame()) continue; try { inFrames += (await frame.$$(selector)).length; } catch { // A frame can detach while we look at it; ignore it. } } console.log(JSON.stringify({ url, selector, ...report, inFrames }, null, 2)); process.exitCode = report.matches > 0 ? 0 : 1; } finally { await browser.close(); }
How to read the result: matches: 0 with inShadow: 1 means a shadow root. matches: 1 with firstVisible: false means the element exists but has no size, for example display: none until a click. If every count is 0, the element is not on the page that loaded.
Your machine is not the renderer, so a match here is strong evidence, not proof. A site can serve different content to cloud IP addresses.
Handle it in code
When an element capture is nice to have, fall back to a viewport screenshot and record that the selector missed:
// element-or-viewport.mjs (Node 22, no dependencies) const API = process.env.RS_BASE_URL ?? 'https://api.renderscreenshot.com'; async function post(body) { const res = await fetch(`${API}/v1/screenshot`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(90_000), }); if (res.ok) return { image: Buffer.from(await res.arrayBuffer()) }; return { error: (await res.json()).error }; } // Capture one element; if the page no longer has it, fall back to the viewport and say so. export async function elementOrViewport(url, selector) { const first = await post({ url, capture: { mode: 'element', selector, selector_required: true }, wait: { until: 'networkidle' }, }); if (first.image) return { image: first.image, matched: true }; const missing = first.error.code === 'selector_not_found' || /selector not found/i.test(first.error.message); if (!missing) throw new Error(`${first.error.code}: ${first.error.message}`); console.warn(`"${selector}" not found on ${url}; using a viewport capture instead`); const second = await post({ url }); if (second.error) throw new Error(`${second.error.code}: ${second.error.message}`); return { image: second.image, matched: false }; }
The failed attempt does not use a credit, so the fallback costs one credit in total.
Tested on 2026-10-15 with Node 22.18. find-selector.mjs ran with Puppeteer 24.43.1 and Chrome 148 against https://example.com (h1: 0 matches; p and svg: found) and against test pages with a shadow root and a hidden element. The fallback ran against a local mock that returns the API's error envelope.
Related
- Capture parameters:
mode,selectorandselector_required. - Wait parameters:
for_selector,for_selector_modeandtimeout.