Pagewright: Verified Page Screenshots
Pricing
$10.00 / 1,000 verified pages
Pagewright: Verified Page Screenshots
Full-page and viewport PNG screenshots, checked before they're returned. Blank renders, soft 404s served under HTTP 200, and error pages are refused with a reason instead of being passed off as successes. You aren't billed for a refused page.
Pricing
$10.00 / 1,000 verified pages
Rating
0.0
(0)
Developer
Brenden Bushman
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a month ago
Last modified
Categories
Share
Screenshots that are checked before they are returned, and refusals are never billed.
A screenshot Actor that hands back a blank image, a soft 404, or an error page has succeeded by every
metric the platform records: the run exits SUCCEEDED, the dataset has a row, the image is there. The
caller finds out later, usually by looking. Pagewright measures the render and refuses the ones that
are not worth having.
What it does
- Full-page or viewport PNG screenshots, configurable viewport width, height and device scale.
- Batch: 1–500 URLs per run, one dataset item per URL whatever the outcome.
- Wait strategy: network-idle (bounded, so it captures anyway if the network never settles, since plenty of ordinary pages hold a connection open indefinitely) or a fixed delay.
- Four checks on every render, each reported separately.
- A refused page emits no billable event at all. You are not charged for a screenshot the Actor itself would not stand behind.
The checks
| Check | What it measures | Refuses? |
|---|---|---|
httpOk | the response status was below 400 | yes → HTTP_ERROR |
nonBlank | the image carries detail rather than being near-uniform | yes → BLANK_RENDER |
notErrorPage | the title and body lead carry no generic error-page wording | yes → ERROR_PAGE |
consentDetected | a known consent platform was present | no, flag only |
Two more refusals come from the browser itself: TIMEOUT and BLOCKED_BY_SITE. Those five are the
whole vocabulary, and nothing outside it is ever returned.
Consent walls are flagged, not refused. They are common enough that refusing them would make the Actor useless, and you are better placed than we are to decide whether a cookie banner matters for your screenshot.
A check that could not run reports null, never true. If the response never arrived, or the image
could not be decoded, or the page text could not be read, the check says so. In strict mode a null
refuses, for the same reason a false does: neither one is evidence that the render is good.
Input
| Field | Type | Default | Notes |
|---|---|---|---|
urls | string[] | none | required, 1 to 500 |
fullPage | boolean | false | whole scrollable page instead of the viewport |
viewport | object | 1280×800 @1× | width, height, deviceScaleFactor |
waitStrategy | networkIdle | delay | networkIdle | |
waitMs | integer | 1000 | used by delay |
timeoutMs | integer | 30000 | per page |
verification | strict | off | strict | see below |
A value outside its declared bound is refused, not clamped. If your input gets quietly corrected, what ran is no longer what you asked for.
verification: "off"
Every check still runs and every result is still reported. off only stops them refusing. Opting out
of enforcement is not opting out of information, so you can see exactly what a lenient run let through.
A browser-level failure (timeout, blocked fetch) is still not a success.
Output
One dataset item per URL:
{"url": "https://example.com","status": "ok","refusalReason": null,"errorText": null,"imageUrl": "https://api.apify.com/v2/key-value-stores/.../records/screenshot-0000.png","httpStatus": 200,"renderedAt": "2026-07-30T18:20:11+00:00","bytes": 48213,"dimensions": { "width": 1280, "height": 800 },"checks": {"httpOk": true,"nonBlank": true,"notErrorPage": true,"consentDetected": false}}
status is ok, refused, or error. The checks object is present and complete on all three,
so you can always see what was actually measured rather than just the verdict.
A refusal looks like this, and costs nothing:
{"url": "https://example.com/missing","status": "refused","refusalReason": "ERROR_PAGE","errorText": null,"imageUrl": null,"httpStatus": 200,"checks": { "httpOk": true, "nonBlank": true, "notErrorPage": false, "consentDetected": false }}
Note the httpStatus: 200. That case is the reason this Actor exists.
A render that failed outright says why, and also costs nothing:
{"url": "https://example.com/unreachable","status": "error","refusalReason": null,"errorText": "net::ERR_NAME_NOT_RESOLVED; unmeasured: body_text, png, title","imageUrl": null,"httpStatus": null,"checks": { "httpOk": null, "nonBlank": null, "notErrorPage": null, "consentDetected": null }}
On an error every measurement is absent, which is what makes it an error rather than a refusal, so
errorText is the whole of what you have to go on. Read it, don't parse it: the wording can change
between runs and carries no compatibility promise. Branch on status and refusalReason.
Pricing
Pay-per-event. page-rendered is charged once per verified success. Refusals and errors don't
emit a billable event at all, so there's nothing to charge for.
Limits
- PNG only. No JPEG, WebP or PDF.
- Consent walls are detected, not dismissed.
- No authenticated pages, no proxy rotation, no anti-bot evasion. You supply the URL; if a site blocks
automated fetches, you get
BLOCKED_BY_SITEand no charge. notErrorPagereads the title plus the first 600 characters of body text. An error page leads with its message. Scanning a whole long article for the same phrases would start refusing pages that simply discuss outages.- The blankness check is a near-uniformity measure. A page whose entire content amounts to a few dozen pixels is treated as blank, because at that point you can't tell the two apart.
Development
py -3.14 -m venv .venv.venv\Scripts\pip install -r requirements.txt.venv\Scripts\python -m playwright install chromium.venv\Scripts\python tools\run_tests.py # the whole guard suite.venv\Scripts\python tools\ast_guard.py # structural guards over the AST.venv\Scripts\python tools\build_input_schema.py # regenerate .actor/input_schema.json.venv\Scripts\python tools\calibrate_entropy.py # re-measure the blankness band.venv\Scripts\python tools\live_sample.py # measure the live refusal rate.venv\Scripts\python tools\publication_gate.py # grade the retained sample
docs/ACCEPTANCE.md records which guard holds which clause of the governing contract, and what is deliberately not proven.

