Docs

Signed Requests

Whenever you share a request URL publicly (via HTML, email, chat, etc.), you essentially expose your access key. As a result, anyone could take your access key and start requesting screenshots that would count against your account's quota.

To prevent this, do the following:

  1. Start signing every request
  2. Require signing for all requests

This way, even if your access key is exposed, whoever gets access to it won't be able to capture screenshots of other URLs, unless your secret key is somehow exposed as well.

Note that signing requests is only required when you plan to share request URLs publicly, and is generally not required when you call the API from the server-side only.

Start signing every request

Regular, unsigned request URLs look like this (unencoded for readability):

https://api.screenshotscout.com/v1/capture?access_key=YOUR_ACCESS_KEY&url=https://example.com/

To start signing every request, follow these steps:

  1. Collect all query params, except signature itself. Example (unencoded for readability): access_key=YOUR_ACCESS_KEY&url=https://example.com/

  2. Build a canonical query string from those params:

    • Sort all params by param name in ascending order.
    • If a param is specified multiple times (e.g., headers or cookies), keep the original order of those params.
    • URL-encode keys and values.
    • Join them as key=value pairs separated by &.

    Example: access_key=YOUR_ACCESS_KEY&url=https%3A%2F%2Fexample.com%2F

  3. Compute an HMAC-SHA256 of the canonical query string using your secret key. Conceptually:

    signature = hex(HMAC_SHA256(secret_key, canonical_query_string))

  4. Append the signature param to your request URL. Example (unencoded for readability):

https://api.screenshotscout.com/v1/capture?access_key=YOUR_ACCESS_KEY&url=https://example.com/&signature=YOUR_SIGNATURE_HEX

CLI example (OpenSSL)

After you build the canonical query string, you can compute the signature in a shell like this:

# Canonical query string (sorted, URL-encoded, no `signature`)
CANONICAL_QUERY='access_key=YOUR_ACCESS_KEY&url=https%3A%2F%2Fexample.com%2F'

# Compute HMAC-SHA256(secret_key, CANONICAL_QUERY) as lowercase hex
echo -n "$CANONICAL_QUERY" | openssl sha256 -hmac "YOUR_SECRET_KEY"

Code examples

Below are examples that build the canonical query string from params, compute the signature, append the signature to the URL, and then call the API.

import { writeFile } from "node:fs/promises";
import crypto from "node:crypto";

const accessKey = "YOUR_ACCESS_KEY";
const secretKey = "YOUR_SECRET_KEY"; // keep this secret
const targetUrl = "https://example.com/";

// Base endpoint (no query yet)
const endpoint = new URL("https://api.screenshotscout.com/v1/capture");

// 1) Build params you will send
const params = new URLSearchParams();
params.set("access_key", accessKey);
params.set("url", targetUrl);

// 2) Build canonical query string (sorted, without `signature`)
function buildCanonicalQuery(params) {
  const entries = [];
  let i = 0;

  for (const [key, value] of params) {
    if (key === "signature") continue;
    entries.push([key, value, i++]);
  }

  entries.sort((a, b) => {
    if (a[0] === b[0]) return a[2] - b[2]; // preserve order for duplicates
    return a[0] < b[0] ? -1 : 1;
  });

  const usp = new URLSearchParams();
  for (const [key, value] of entries) usp.append(key, value);
  return usp.toString();
}

const canonicalQuery = buildCanonicalQuery(params);

// 3) Compute HMAC-SHA256(secretKey, canonicalQuery) as lowercase hex
const signature = crypto
  .createHmac("sha256", secretKey)
  .update(canonicalQuery, "utf8")
  .digest("hex");

// 4) Build final request URL: canonical query + signature
const finalUrl = new URL(endpoint);
finalUrl.search = `${canonicalQuery}&signature=${signature}`;

const response = await fetch(finalUrl);
if (!response.ok) {
  throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}

const bytes = Buffer.from(await response.arrayBuffer());
await writeFile("screenshot.png", bytes);
from urllib.parse import urlencode, urlsplit, urlunsplit
from urllib.request import urlopen
import hmac
import hashlib

access_key = "YOUR_ACCESS_KEY"
secret_key = "YOUR_SECRET_KEY"  # keep this secret
target_url = "https://example.com/"

base = urlsplit("https://api.screenshotscout.com/v1/capture")

# 1) Parameters you will send (list of pairs to preserve order / allow duplicates)
params = [
    ("access_key", access_key),
    ("url", target_url),
]

# 2) Build canonical query string (sorted by key, without `signature`)
def build_canonical_query(pairs):
    filtered = [(k, v) for (k, v) in pairs if k != "signature"]
    # Python's sort is stable, so duplicates keep original order
    filtered.sort(key=lambda item: item[0])
    return urlencode(filtered, doseq=True)

canonical_query = build_canonical_query(params)

# 3) Compute HMAC-SHA256(secret_key, canonical_query) as lowercase hex
signature = hmac.new(
    secret_key.encode("utf-8"),
    canonical_query.encode("utf-8"),
    hashlib.sha256,
).hexdigest()

# 4) Build final query string and request URL
query = f"{canonical_query}&signature={signature}"
endpoint = urlunsplit((base.scheme, base.netloc, base.path, query, base.fragment))

with urlopen(endpoint) as response:
    image_bytes = response.read()

with open("screenshot.png", "wb") as f:
    f.write(image_bytes)
<?php

$accessKey = 'YOUR_ACCESS_KEY';
$secretKey = 'YOUR_SECRET_KEY'; // keep this secret
$targetUrl = 'https://example.com/';

$endpoint = 'https://api.screenshotscout.com/v1/capture';

// 1) Parameters you will send
$params = [
    'access_key' => $accessKey,
    'url'        => $targetUrl,
    // add more options here as needed
];

// 2) Build canonical query string (sorted by key, without `signature`)
unset($params['signature']); // just in case
ksort($params); // sort by parameter name (keys)

$canonicalQuery = http_build_query(
    $params,
    '',
    '&',
    PHP_QUERY_RFC1738 // spaces encoded as "+" like URLSearchParams
);

// 3) Compute HMAC-SHA256(secretKey, canonicalQuery) as lowercase hex
$signature = hash_hmac('sha256', $canonicalQuery, $secretKey);

// 4) Final request URL: canonical query + signature
$requestUrl = sprintf('%s?%s&signature=%s', $endpoint, $canonicalQuery, $signature);

$ch = curl_init($requestUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$bytes = curl_exec($ch);
curl_close($ch);

file_put_contents('screenshot.png', $bytes);

Require signing for all requests

After you have started signing every request, you now need to require signed requests.

Go to the API Keys tab in the panel, click the edit button, then enable the "Require signed requests" toggle. Click "Save".

Now, every request URL must include a valid signature param. If the signature param is not provided, or if the signature is invalid, requests will be rejected with a signature_required or signature_invalid error accordingly.