Fail a screenshot if the page contains text
A screenshot API that returns HTTP 200 with a picture of a login form, a "Page not found" template or an "Access denied" notice has done what you asked, and it is still the wrong result. Content checks turn those cases into errors. You list text that must be on the page, text that must not be, or both, and the request fails with content_check_failed instead of returning the wrong image. More guides are on the guides hub.
How the checks work
Content checks live in the options group of a POST body:
| Parameter | Type | Fails the request when |
|---|---|---|
options.fail_if_missing |
array of strings | Any string in the list is not on the page |
options.fail_if_contains |
array of strings | Any string in the list is on the page |
options.fail_on_error |
boolean | The page logs a console error or throws an uncaught exception |
Matching is case-sensitive and partial, against the page's visible text (document.body.innerText) after the wait settings. "Sign in" matches "Sign in to continue" but not "SIGN IN" or "sign in". Text hidden with CSS does not count. The two text checks fail with content_check_failed; fail_on_error fails with its own code, console_error (HTTP 422, retryable: false). The content validation reference has the full rules.
A failed check returns HTTP 422:
{ "error": { "type": "target_error", "code": "content_check_failed", "message": "Forbidden text found: 'Sign in'", "retryable": true } }
The minimal request
Content checks are POST only. This request passes on https://example.com, because the page contains "Example Domain" and neither forbidden phrase:
curl
curl -sS --fail -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "options": { "fail_if_missing": [ "Example Domain" ], "fail_if_contains": [ "Sign in", "Page not found" ] } }' \ -o example.png
Node.js
import { writeFile } from 'node:fs/promises'; const body = { "url": "https://example.com", "options": { "fail_if_missing": [ "Example Domain" ], "fail_if_contains": [ "Sign in", "Page not found" ] } }; const res = await fetch('https://api.renderscreenshot.com/v1/screenshot', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!res.ok) { console.error(res.status, await res.text()); process.exit(1); } await writeFile('example.png', Buffer.from(await res.arrayBuffer()));
Python
import json import os import sys import urllib.error import urllib.request body = { 'url': 'https://example.com', 'options': { 'fail_if_missing': ['Example Domain'], 'fail_if_contains': ['Sign in', 'Page not found'], }, } req = urllib.request.Request( 'https://api.renderscreenshot.com/v1/screenshot', data=json.dumps(body).encode(), headers={ 'Authorization': f"Bearer {os.environ['RS_API_KEY']}", 'Content-Type': 'application/json', }, method='POST', ) try: with urllib.request.urlopen(req, timeout=90) as res: with open('example.png', 'wb') as f: f.write(res.read()) except urllib.error.HTTPError as err: sys.exit(f'{err.code} {err.read().decode()}')
Ruby
require 'json' require 'net/http' body = { url: 'https://example.com', options: { fail_if_missing: ['Example Domain'], fail_if_contains: ['Sign in', 'Page not found'] } } uri = URI('https://api.renderscreenshot.com/v1/screenshot') request = Net::HTTP::Post.new(uri) request['Authorization'] = "Bearer #{ENV.fetch('RS_API_KEY')}" request['Content-Type'] = 'application/json' request.body = body.to_json response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', read_timeout: 90) do |http| http.request(request) end abort("#{response.code} #{response.body}") unless response.is_a?(Net::HTTPSuccess) File.binwrite('example.png', response.body)
PHP
<?php $body = [ 'url' => 'https://example.com', 'options' => [ 'fail_if_missing' => ['Example Domain'], 'fail_if_contains' => ['Sign in', 'Page not found'], ], ]; $ch = curl_init('https://api.renderscreenshot.com/v1/screenshot'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RS_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 90, ]); $image = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if ($image === false || $status !== 200) { fwrite(STDERR, $status . ' ' . ($image === false ? curl_error($ch) : $image) . "\n"); exit(1); } file_put_contents('example.png', $image);
Go
package main import ( "bytes" "io" "log" "net/http" "os" ) const body = `{ "url": "https://example.com", "options": { "fail_if_missing": [ "Example Domain" ], "fail_if_contains": [ "Sign in", "Page not found" ] } }` func main() { req, err := http.NewRequest(http.MethodPost, "https://api.renderscreenshot.com/v1/screenshot", bytes.NewBufferString(body)) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RS_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() data, err := io.ReadAll(res.Body) if err != nil { log.Fatal(err) } if res.StatusCode != http.StatusOK { log.Fatalf("%d %s", res.StatusCode, data) } if err := os.WriteFile("example.png", data, 0o644); err != nil { log.Fatal(err) } }
Java
import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; public class Capture { public static void main(String[] args) throws Exception { String body = """ { "url": "https://example.com", "options": { "fail_if_missing": [ "Example Domain" ], "fail_if_contains": [ "Sign in", "Page not found" ] } } """; HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.renderscreenshot.com/v1/screenshot")) .header("Authorization", "Bearer " + System.getenv("RS_API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<byte[]> response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofByteArray()); if (response.statusCode() != 200) { System.err.println(response.statusCode() + " " + new String(response.body())); System.exit(1); } Files.write(Path.of("example.png"), response.body()); } }
Every example exits with a non-zero status on any HTTP error and prints the JSON error, so a failed check stops a script or a CI step. The curl example uses --fail, which hides the body. Remove --fail to read the error message while you debug.
Recipe: a page behind a login
When a session expires, most apps redirect to a sign-in form and return 200. Check for one phrase that only the signed-in page has, and one that only the sign-in form has:
{ "url": "https://app.example.com/reports/weekly", "options": { "fail_if_missing": ["Weekly report"], "fail_if_contains": ["Sign in to your account", "Session expired"] } }
Use both lists. fail_if_missing alone also fails for a reason you do not expect, such as a renamed heading. fail_if_contains alone misses a blank page.
Recipe: soft 404 pages
Many sites answer a missing page with status 200 and a friendly template. To find the right phrase, open a URL that does not exist on that site, such as /this-page-does-not-exist, and copy a sentence from the template:
{ "url": "https://shop.example.com/products/discontinued-item", "options": { "fail_if_contains": ["We couldn't find that page", "This product is no longer available"] } }
Avoid short, generic strings. Because matching is partial, "404" also matches "Order #40412" and "Call 404-555-0100", and "Error" matches "Error handling guide". A full sentence from the site's own error template is safe.
Recipe: a dashboard that loads data late
A data page can render its frame before the numbers arrive. Combine a wait with checks for the loaded state and for the loading text:
{ "url": "https://status.example.com", "wait": { "until": "networkidle", "delay": 1500 }, "options": { "fail_if_missing": ["All systems"], "fail_if_contains": ["Loading", "Something went wrong"], "fail_on_error": true } }
fail_on_error is strict. Third-party scripts often log console errors on healthy pages. Turn it on only for pages where you control every script.
Retry or not
The error is marked retryable: true, because some failures are timing problems. Decide by recipe:
| Cause | Retry? |
|---|---|
| Loading text still on the page | Yes, once, with a longer wait.delay |
| Sign-in form on the page | No. Refresh the session first. |
| Site's 404 template on the page | No. The page is gone. |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Fails on a page that looks fine | A short string matched by accident | Use a full sentence from the error template. |
| Passes on a page with an error | Case differs ("Not Found" vs "Not found") | Copy the exact text from the page. |
fail_if_missing fails on a text you can see |
The text is in an image, a canvas or an iframe | Pick text that is in the page's own HTML. |
fail_if_contains passes although the text is in the HTML |
The text is hidden with CSS, so it is not in the visible text | Match text that a visitor can see. |
console_error instead of content_check_failed |
fail_on_error found a console error or an uncaught exception |
Remove fail_on_error for pages with third-party scripts. |
| Fails at random | The text appears after the check | Add wait.until: "networkidle" or a wait.delay. |
| Ignored with GET | Checks are POST only | Send a POST body. |
Related
- Run the CLI in CI to capture pages after each deploy.
- Screenshot authenticated pages to keep the session valid.
- Wait strategies
- Error codes
- Create a free account to test checks against your own pages.