Documentation

From an empty account to a regression report.

Everything here describes what the product does today. Where a capability depends on your plan, the plan is named — see pricing for the full gate list.

Getting started

Six steps to your first diff.

Create your organisation

Sign up with an email address, a password and an organisation name. Verify the email — until you do, the account can sign in but cannot enqueue jobs that fetch external sites. Your 14-day trial starts at verification.

Register a site

A site is one property you monitor. Give it a name and the URL of each environment you have: production, staging, dev. Each environment can carry its own HTTP authentication and its own SOCKS5 proxy, or explicitly none.

Choose the URLs

Run discovery to read the sitemap and crawl for common paths, then keep the pages worth watching — usually the homepage, the templates (one of each), and the pages a client would notice immediately. Each tracked URL keeps a stable id, so results match up across runs even if you rename or reorder.

Pick viewports, and record actions if you need them

Viewports are global presets referenced by name, so “desktop” means the same width everywhere. A URL can be filtered to only some of them. If a page needs a cookie banner dismissed or a menu opened before the screenshot, record those actions with the Chrome extension — included on every plan, Free included — or add them by hand.

Capture a baseline

Queue a baseline run. Every tracked URL is rendered at every selected viewport and stored as the reference of record. Look through it once — a baseline captured while the site was mid-deploy will haunt you for weeks.

Compare, review, classify

Queue a comparison after a deploy, or let a schedule do it (Studio and above). Review each result side by side, in overlay, or with onion-skin; mark false positives and intentional content changes so the next review is quieter.

Concepts

The eight words you need.

Site & environment

A site belongs to exactly one organisation. Its environments are the same site at different URLs — production, staging, dev — each with its own auth and proxy.

Run

One execution, identified by a timestamp. Runs are per environment and have a type: baseline, comparison or console.

Baseline

The reference screenshots a comparison is measured against. You can re-baseline at any time; the previous baseline stays inside its own run.

Diff

The percentage of pixels that differ between a baseline image and the current one, plus a highlight image showing where. Reported per URL and viewport.

Shift tolerance

When content is simply displaced vertically, the comparator detects the shift and reports a corrected diff alongside the raw one, so “we added a banner” does not read as “forty pages broke”.

Action

A pre-screenshot browser interaction attached to a URL: click, hover, scroll, type, wait, waitForSelector. Recorded in the browser or written by hand.

Job & queue

Work is queued, not run inline: baseline, comparison, console, retake, thumbnail, analyze. The queue page shows status and history. One queue, first in first out — there is no priority lane on any plan.

FP / CC

Review verdicts. FP — false positive, not a real change. CC — content change, real but intentional. Both quiet the result without hiding it.

Analysis

An AI reading of one diff, returning a classification and an explanation. Metered per plan: on a plan without AI the platform refuses to run one, so no image is sent. Any organisation can also switch AI off entirely under Configs → AI, on every plan including Free; while it is off every analysis is refused server-side, so nothing is sent at all.

Reference

Deeper material.

REST API reference →

The full endpoint reference lives inside your dashboard, because it is generated against your own organisation, your keys and your rate limits.

  • Authentication with a scoped API key
  • Sites, URLs, runs and results
  • Enqueueing jobs and reading their status
  • Rate-limit headers and error codes

API keys →

Create, scope and revoke keys. A key is shown once, at creation, and stored only as a hash — if you lose it, roll a new one.

  • Read access from Studio, write access from Agency
  • Per-plan request rate limits, counted per hour
  • Revocation takes effect immediately

Usage & limits →

What you have consumed this period against what your plan allows, per metric. This page is the only place that number lives — there are no usage-threshold emails yet, so nothing warns you at 80%.

  • Screenshots in the rolling window
  • AI analyses this month
  • Sites and seats in use

Chrome extension

An in-page drawer showing whether the page you are on is tracked, a queue badge, and the action recorder. Both the drawer and the recorder are included on every plan, Free included — they are never gated.

  • Install, then point it at your dashboard URL
  • Record clicks, hovers, scrolls and typing
  • Hover states are detected even when CSS-only
  • Exclude URLs where the drawer should never appear

Scheduling & run notifications

Studio and above can run comparisons on a cadence — a gate the platform genuinely enforces. Results land in the same run history as manual runs.

Every run, scheduled or manual, is announced by email when it reaches a terminal state. Everyone in the organisation with the “Run finished” preference on gets one message — site, run, status, screenshots compared, differences found, link into the run — and a second “visual differences found” message when the run actually reported at least one diff, listing the largest by page and viewport. Both preferences live on your Account page, both are on by default, both carry a one-click unsubscribe, and neither is gated by plan. There are still no outgoing webhooks, no Slack notifications and no usage-threshold emails: poll the API for those.

  • Daily or weekly cadence per site and environment
  • Runs are queued, so a busy period delays rather than drops them
  • A run is announced once: a re-import or a retake never re-sends it

Troubleshooting

The failures we see most often, and what they usually mean.

  • Everything diffs — the baseline was captured mid-deploy, or a cookie banner appears only sometimes. Re-baseline, or record an action to dismiss it.
  • One viewport diffs — usually a lazy-loaded image that had not settled. Add a short wait action.
  • Staging times out — check the environment's proxy and HTTP auth; a proxy set at site level applies unless the environment overrides it.
  • Jobs stay pending — the account's email is unverified, or a plan limit has been reached. Both are stated on the queue page.

Something missing or wrong here? Tell us — documentation gaps are treated as product bugs.

REST API

The public API, in full.

Generated from the running code, so it cannot drift from what the server actually accepts. Read access is available from Studio, write access from Agency.

Overview

The SBS public API is a read-mostly JSON API over HTTPS. It exposes the sites, runs, run results and queue jobs of exactly one organization: the one the API key belongs to. A key can never reach another organization's data.

Base URL https://sbspx.com/wp-json/sbs/public/v1

The version is part of the path (/sbs/public/v1). Breaking changes ship as a new version; additive fields may appear inside v1 without notice, so parse defensively and ignore keys you do not recognise.

Authentication

Every request carries a bearer key. Cookies and WordPress nonces are not accepted on this namespace — the API is for machines.

Authorization: Bearer sbs_live_YOUR_KEY_HERE

Keys look like sbs_live_… in production and sbs_test_… everywhere else, followed by 32 random characters. Only a SHA-256 hash of the key is stored, so the plaintext is shown exactly once, at creation, and can never be recovered.

Treat a key like a password: put it in an environment variable, never in a git repository or a browser.

Scopes

Each key carries an explicit list of scopes. A route refuses a key that lacks its scope, with sbs_forbidden.

ScopeGrantsKind
sites:read Read sites and their configuration read
sites:write Modify site configuration (reserved — no write route uses it yet) write
runs:read Read runs and run results read
runs:write Trigger runs (enqueue baseline / comparison / console jobs) write
queue:write Manage queue jobs write
artifacts:read Read screenshot artifact metadata read
usage:read Read usage and quota figures read

Plan availability

API access is a plan feature. Read access and write access are gated separately, and the hourly rate limit is per plan.

PlanReadWriteRequests / hour / key
Free no no 600
Starter no no 1,200
Studio yes no 3,000
Agency yes yes 10,000
Scale yes yes 30,000

Rate limits

Limits are counted per key in a fixed one-hour window. Every response — success or failure — carries the current state:

HeaderMeaning
X-RateLimit-LimitRequests allowed in the window. 0 means unlimited.
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetUnix timestamp at which the window rolls over.
Retry-AfterSeconds to wait. Sent only on a 429.
X-Request-IdCorrelation id for this request. Quote it in support tickets.

Refused requests still count against the window, so back off on 429 rather than retrying immediately.

Pagination

List routes accept page (1-based, default 1) and per_page (1–100, default 25). They return the totals both in the body (page, perPage, total, totalPages) and in the X-Total-Count / X-Total-Pages headers.

Errors

Errors are always JSON with a stable machine-readable code:

{
  "code": "sbs_rate_limited",
  "message": "Rate limit exceeded: 600 requests per hour. Retry in 812 seconds.",
  "data": { "status": 429, "retryAfter": 812 }
}

Branch on code, never on the message text.

CodeHTTPWhen
sbs_api_key_invalid 401 The Authorization header is missing, malformed, or the key is unknown, revoked or expired.
sbs_plan_limit 403 The organization's plan does not include this capability (no API access, or read-only when a write was attempted), or its subscription state has withdrawn it. data.reason says which.
sbs_forbidden 403 The key is valid but does not carry the scope the route requires. data.requiredScope names it.
sbs_email_unverified 403 The account that created this key has not confirmed its email address, so the platform will not fetch pages on its behalf. Only write routes are affected; reads keep working.
sbs_not_found 404 The site, run or job does not exist inside the key's organization. Cross-tenant identifiers return 404, never 403.
sbs_bad_request 400 A parameter is missing or invalid — for example an unknown run action.
sbs_conflict 409 The request was understood but refused by a business rule, e.g. a comparison with no baseline yet, or a plan limit reached at enqueue time.
sbs_rate_limited 429 The hourly quota for this key is spent. Retry-After says how many seconds to wait.
sbs_api_disabled 503 The public API is switched off on this deployment.

Two things the table does not say on its own

Subscription state arrives as sbs_plan_limit. There is no separate sbs_trial_expired or sbs_subscription_required code on this namespace. An expired trial or a canceled subscription drops the organization back to the free tier's entitlements, which do not include API access, so the refusal comes back as 403 sbs_plan_limit like any other missing capability. Read data.reason to tell the cases apart — it is one of plan_feature_unavailable, org_suspended or org_not_found — and data.upgradeUrl where an upgrade would fix it.

The organization's “disable AI analysis” switch does not change any response here. No route in this API requests an AI analysis: POST /sites/{identifier}/runs accepts only baseline, comparison and console. When the switch is off, an API-triggered comparison is still accepted and still runs; what is refused is the automatic analysis that would otherwise follow it, inside the platform, before any image is sent — so the run's results simply come back without an analysis. That refusal is what clause 11 of the Data Processing Agreement means when it says the platform refuses the analysis for a run started through the public API: the analysis is refused, not the run. The platform reserves the code sbs_ai_disabled (403, reason org_ai_disabled) for an AI analysis that is requested directly and refused by that switch. No route in this namespace returns it today, and it will be added to the table above on the day one does. It is documented here so that an integrator branching on code knows the name exists and what it would mean, rather than meeting it undocumented.

Quick start

Verify a key and see which organization it is bound to:

export SBS_KEY=sbs_live_YOUR_KEY_HERE
curl -sS -H "Authorization: Bearer $SBS_KEY" \
  https://sbspx.com/wp-json/sbs/public/v1/ping

List your sites, then trigger a comparison on one of them (write scope + Agency plan or above):

curl -sS -H "Authorization: Bearer $SBS_KEY" \
  "https://sbspx.com/wp-json/sbs/public/v1/sites?per_page=10"

curl -sS -X POST \
  -H "Authorization: Bearer $SBS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action":"comparison","environment":"production"}' \
  https://sbspx.com/wp-json/sbs/public/v1/sites/SITE_ID/runs

Then poll the job until it leaves pending/running:

curl -sS -H "Authorization: Bearer $SBS_KEY" \
  https://sbspx.com/wp-json/sbs/public/v1/jobs/JOB_ID

Endpoints

This list is generated from the registered routes, so it always matches what the server serves.

GET /ping any valid key

Verify a key. Returns the organization the key is bound to and the scopes it carries.

Example request

curl -sS -H "Authorization: Bearer $SBS_KEY" \
  https://sbspx.com/wp-json/sbs/public/v1/ping

Example response

{ "ok": true, "version": "1.0.0", "mode": "test", "org": { "id": 9, "slug": "acme", "name": "Acme" }, "scopes": ["sites:read"] }
GET /sites scope sites:read

List every site in the organization the key belongs to.

ParameterDescription
page Page number, 1-based. Default 1.
per_page Items per page, 1-100. Default 25.
enabled Filter on enabled state: 1 or 0. Omit for all.

Example request

curl -sS -H "Authorization: Bearer $SBS_KEY" \
  https://sbspx.com/wp-json/sbs/public/v1/sites

Example response

{ "sites": [ { "id": "…", "slug": "acme-com", "name": "Acme", "enabled": true, … } ], "page": 1, "perPage": 25, "total": 4, "totalPages": 1 }
GET /sites/{identifier} scope sites:read

One site. {identifier} is the site id, its storage id, or its slug.

Example request

curl -sS -H "Authorization: Bearer $SBS_KEY" \
  https://sbspx.com/wp-json/sbs/public/v1/sites/SITE_ID

Example response

{ "id": "…", "slug": "acme-com", "environments": [...], "viewports": [...], "urlCount": 12 }
GET /sites/{identifier}/runs scope runs:read

Runs for one site, newest first.

ParameterDescription
environment Restrict to one environment, e.g. production.
type Restrict to baseline, comparison or console.
page Page number, 1-based. Default 1.
per_page Items per page, 1-100. Default 25.

Example request

curl -sS -H "Authorization: Bearer $SBS_KEY" \
  https://sbspx.com/wp-json/sbs/public/v1/sites/SITE_ID/runs

Example response

{ "runs": [ { "runId": "20260811-091500", "type": "comparison", "summary": {...} } ], "total": 31 }
POST /sites/{identifier}/runs scope runs:write write · Agency+

Trigger a run. The job is queued; poll /jobs/{jobId} for its progress.

ParameterDescription
action Required: baseline, comparison or console.
environment Environment name. Defaults to production.

Example request

curl -sS -X POST \
  -H "Authorization: Bearer $SBS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action":"comparison","environment":"production"}' \
  https://sbspx.com/wp-json/sbs/public/v1/sites/SITE_ID/runs

Example response

{ "jobId": "job-20260811-091500-ab12", "status": "pending", "action": "comparison", "environment": "production" }
GET /runs/{runId} scope runs:read

Run metadata without the per-screenshot results.

ParameterDescription
site Disambiguates when the same run id exists on more than one of your sites.

Example request

curl -sS -H "Authorization: Bearer $SBS_KEY" \
  https://sbspx.com/wp-json/sbs/public/v1/runs/RUN_ID

Example response

{ "runId": "20260811-091500", "site": "acme-com", "type": "comparison", "summary": {...} }
GET /runs/{runId}/results scope runs:read

Per-screenshot results for a run, paginated.

ParameterDescription
site Disambiguates when the same run id exists on more than one of your sites.
status Filter on result status, e.g. diff or error.
page Page number, 1-based. Default 1.
per_page Items per page, 1-100. Default 25.

Example request

curl -sS -H "Authorization: Bearer $SBS_KEY" \
  https://sbspx.com/wp-json/sbs/public/v1/runs/RUN_ID/results

Example response

{ "runId": "…", "results": [ { "path": "/", "viewport": "desktop", "status": "diff", "diffPercentage": 1.4 } ], "total": 24 }
GET /jobs/{jobId} scope runs:read

Status of one queued job.

Example request

curl -sS -H "Authorization: Bearer $SBS_KEY" \
  https://sbspx.com/wp-json/sbs/public/v1/jobs/JOB_ID

Example response

{ "jobId": "job-…", "status": "completed", "runId": "20260811-091500", "error": null }
GET /jobs scope runs:read

Queue jobs for the organization, newest first.

ParameterDescription
status Filter on pending, running, completed or failed.
site Restrict to one site (id, storage id or slug).
page Page number, 1-based. Default 1.
per_page Items per page, 1-100. Default 25.

Example request

curl -sS -H "Authorization: Bearer $SBS_KEY" \
  https://sbspx.com/wp-json/sbs/public/v1/jobs

Example response

{ "jobs": [ { "jobId": "job-…", "action": "comparison", "status": "pending" } ], "total": 3 }
GET /usage scope usage:read

Current billing-period usage and the plan limits it is measured against.

Example request

curl -sS -H "Authorization: Bearer $SBS_KEY" \
  https://sbspx.com/wp-json/sbs/public/v1/usage

Example response

{ "plan": { "slug": "plan-agency", "name": "Agency" }, "limits": {...}, "period": {...} }

Notes and guarantees

  • Tenancy. Site, run and job identifiers are resolved inside the key's organization. An identifier belonging to another organization returns 404, so the API never reveals that it exists.
  • Site identifiers. {identifier} accepts the site's stable storage id, its numeric id, or its slug. Prefer the storage id returned as id — slugs can be renamed.
  • Triggering runs is asynchronous. POST /sites/{identifier}/runs returns 202 with a jobId; the run appears once a queue worker has processed it.
  • Limits of 0 mean unlimited everywhere in this API, including X-RateLimit-Limit.
  • Usage. Every authenticated request is metered as one api_request against the organization and is visible on the usage screen.

Read it later. Try it now.

The first baseline takes about five minutes, and it is the only part that needs thought.

Start free trial