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.
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.
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.
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.
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.
| Scope | Grants | Kind |
|---|---|---|
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.
| Plan | Read | Write | Requests / 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:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the window. 0 means unlimited. |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Unix timestamp at which the window rolls over. |
Retry-After | Seconds to wait. Sent only on a 429. |
X-Request-Id | Correlation 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.
| Code | HTTP | When |
|---|---|---|
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.
/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"] }
/sites
scope sites:read
List every site in the organization the key belongs to.
| Parameter | Description |
|---|---|
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 }
/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 }
/sites/{identifier}/runs
scope runs:read
Runs for one site, newest first.
| Parameter | Description |
|---|---|
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 }
/sites/{identifier}/runs
scope runs:write
write · Agency+
Trigger a run. The job is queued; poll /jobs/{jobId} for its progress.
| Parameter | Description |
|---|---|
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" }
/runs/{runId}
scope runs:read
Run metadata without the per-screenshot results.
| Parameter | Description |
|---|---|
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": {...} }
/runs/{runId}/results
scope runs:read
Per-screenshot results for a run, paginated.
| Parameter | Description |
|---|---|
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 }
/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 }
/jobs
scope runs:read
Queue jobs for the organization, newest first.
| Parameter | Description |
|---|---|
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 }
/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 asid— slugs can be renamed. - Triggering runs is asynchronous.
POST /sites/{identifier}/runsreturns202with ajobId; 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_requestagainst 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