Limits
What counts, what is kept, and for how long.
Limits
What the DOM engine cannot capture
The default engine rebuilds your page from the DOM and computed styles. That is what makes it fast and prompt-free, and it also means a few things are genuinely invisible to it:
| Option | Type | Default | Description |
|---|---|---|---|
Cross-origin iframes | blank | display-media | Same-origin frames are captured. Anything else the browser will not let us read, so it renders empty — Stripe Elements, embedded maps, third-party video players. |
Video frames | poster only | display-media | A <video> renders as its poster, not the current frame. Draw the frame to a canvas yourself first if you need it. |
Tainted canvases | blank | — | A canvas that has drawn a cross-origin image without CORS cannot be read back. Serve those images with Access-Control-Allow-Origin. |
WebGL without preserveDrawingBuffer | blank | — | The buffer is cleared after each frame unless the context was created with preserveDrawingBuffer: true. |
Shadow DOM (closed) | blank | — | Open shadow roots are captured. Closed ones cannot be read at all. |
Native UI | blank | display-media | Select dropdowns while open, the print dialog, browser chrome, extensions — none of it is in the DOM. |
Where the table says display-media, switching engine fixes it: that path captures real pixels including everything above. The trade is a browser permission prompt, and the user choosing what to share.
Browser support
Chrome, Edge, Firefox and Safari, current and previous major versions. PDF output uses CompressionStream where available and falls back to embedding a JPEG where it is not, so the file is always valid — just larger on older Safari. There is no IE support and there will not be.
What the capture counter counts
With uploads off, no webhook fires. Delivery is decided server-side from the stored capture, so if nothing is stored there is nothing to deliver and nothing appears in your dashboard. The render still counts.
The editor reflects this rather than hiding it: its confirm button reads Done instead of Upload, and the SDK logs a one-time console warning — so it is not something you find out from an empty dashboard three days later.
It counts renders, not stored files. Rendering is the work, and a capture you download costs us what one you upload costs, so both count. That makes the number on your dashboard bigger than the list of captures underneath it, for two separate and entirely normal reasons:
- Renders you kept.
download(),toCanvas(),toBlob(), or any capture with uploads off. Counted, never stored, so there is nothing to list. - Renders that expired. Uploaded and stored, then deleted at the end of the retention window below. They stay in the month’s count; they leave the list after three days.
The Overview stat breaks the total into those parts, so the two numbers always reconcile. If you want a render that is not counted at all, the module-level renderToCanvas() has no key and no client and reports nothing.
Retention — three days, seven at most
Captures are deleted from our storage after three days. Set retention_days on a project to choose anything from one to seven. Seven is the ceiling for every account and no plan raises it — we hold pictures of your users’ screens, and a window we can defend is worth more than one that flatters a pricing page.
What that means in practice:
| Option | Type | Default | Description |
|---|---|---|---|
Signed URLs | string | capture life | Clamped to the life of the file they point at, so a project on three-day retention never receives a seven-day link. A URL that looks valid and 404s when you finally use it is worse than a short one. |
Failed deliveries | resendable | capture life | Kept with their response so you can resend by hand for as long as the files exist. Nothing retries automatically. |
Capture record | row | 3–7 days | Removed with the files. Dashboard history goes with it — usage counts survive, aggregated by day. |
So if you need a capture kept, copy it somewhere of your own when the webhook arrives. Do not treat a signed URL as durable storage and do not store one for later — fetch the capture by id with a secret key instead, which mints a fresh URL for as long as the capture exists.
Size and quota
Output is capped at maxDimension pixels per side (16384 by default) because browsers refuse to allocate canvases much beyond that. A very long page is scaled to fit rather than failing. Per-project monthly capture limits are set in Settings; exceeding one returns rate_limited rather than silently dropping captures.
Content Security Policy
The SDK inlines images and fonts as data URLs and rasterizes through an SVG foreignObject, so a strict CSP needs to allow both:
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self' https://api.screen2api.com;If you load the bundle from our CDN rather than your own, add https://screen2api.com to script-src and keep the integrity hash — it is what makes that safe.