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 in this topic

Was this page helpful?