Page reports API reference
Request and response shapes for the five inference-free report endpoints on a session: consent, agent view, destinations, cancellation and screenshot.
Last updated:
Five endpoints on a live session answer structural questions about a page without ever calling a model: which consent platform is showing and what the browser held before any choice, what the agent's tree looks like, where every control leads, whether a cancellation control is reachable, and a picture of the page. All five are POSTs against a session id carrying your bb_ key as a bearer token, and all five are computed from the same perception the agent runs on — which is why they still work on a deployment that has sessions and no inference gateway at all.
They arrived in protocol 1.5. A client that announces x-browserberg-protocol: 1.5.0 to an older server is refused with protocol_incompatible rather than discovering the gap route by route. A deployment that offers them lists reports in the features array of GET /v1/me; one that does not never registers the routes, so a call gets not_found.
Three failures are worth branching on. A body that fails validation is invalid_request (400) with the first ten schema issues in details.issues. A session that does not exist, or belongs to another organisation, is not_found (404) — the same answer for both, deliberately. A URL the browser refuses to open, for DNS, TLS or a blocked scheme, is navigation_failed (422). A page that simply never finishes loading is not a failure at all: after fifteen seconds the report is taken as the page stands and a line is added to warnings.
/v1/sessions/:id/consent-report
Which consent platform is showing, what the browser held before any choice, and what a reject changed.
Consent report request
| Name | Type | Description |
|---|---|---|
url
|
string | An absolute http or https URL, at most 2048 characters, opened before the report is taken. Omit it to report on the page the session is already on. |
policy
|
string |
`reject`, `accept` or `detect-only`. Under `detect-only` nothing on the page is touched, so every after-choice half of the response comes back null.
Default: reject
|
waitMs
|
integer |
How long to keep looking for a banner that paints late, 0 to 15000. The search polls every 250 ms and stops at the first banner it recognises.
Default: 5000
|
screenshot
|
boolean |
Adds a viewport capture to the response. Fixed at JPEG quality 60 here; the screenshot endpoint is the one that takes options.
Default: false
|
Consent report response
| Name | Type | Description |
|---|---|---|
url
|
string | The URL you asked for, or the page the session was found on. |
finalUrl
|
string | Where the page ended up once redirects had been followed. |
cmp
|
string | null | The recognised platform — `cookiebot`, `usercentrics`, `onetrust`, `didomi` and a dozen more — or `heuristic` when an unrecognised banner was matched by its labels, or null when no banner was seen. |
bannerDetectedAtMs
|
integer | null | Milliseconds after navigation at which a banner was first seen. Null when none was. |
dismissal
|
object | null | Null under `detect-only`. Otherwise `{dismissed, method, actedOn?, rounds, stubborn, durationMs}`, where `method` is `click`, `api` or `none`, `rounds` counts the layers it took, and `stubborn` means a banner was recognised, acted on and is still there. |
cookies
|
object | `{beforeChoice, afterChoice}`, each an array of cookie rows. `afterChoice` is null under `detect-only`. |
requests
|
object | `{beforeChoice, afterChoice}`, each an array of host rows. Same null rule as cookies. |
summary
|
object | `{cookiesBeforeChoice, thirdPartyCookiesBeforeChoice, thirdPartyHostsBeforeChoice, newCookiesAfterReject}`. The last is null unless the policy was `reject`. |
screenshot
|
object | Present only when you asked for one: `{contentType, base64, width?, height?}`. |
warnings
|
string[] | What the report could not do cleanly — a load that never finished, cookies the browser would not hand over, a banner that resisted. |
durationMs
|
integer | Wall-clock time the report took, including the navigation. |
/v1/sessions/:id/agent-view
The page as the agent reads it, rendered at up to three fidelities, with the interactive controls it found.
Agent view request
| Name | Type | Description |
|---|---|---|
url
|
string | Opened first, as above. Omit it for the page the session is on. |
fidelities
|
string[] |
One to three of `full`, `economy` and `lean`. Every entry renders the SAME tree; only how much is elided differs, so the element ids match across renderings.
Default: [full, economy, lean]
|
screenshot
|
boolean |
Adds a viewport JPEG at quality 60.
Default: true
|
maxChars
|
integer | 1000 to 400000, a ceiling applied to each rendered view separately. Truncation is announced in `views[].truncated`, never silent. |
consent
|
string |
`reject`, `accept` or `off` — how a banner is handled before the page is read.
Default: reject
|
Agent view response
| Name | Type | Description |
|---|---|---|
url
|
string | The page that was read. |
views
|
array | One entry per requested fidelity: `{fidelity, text, chars, lines, truncated}`. |
controls
|
array | One entry per interactive element, without its subtree: `{encodedId, role, name, tag, jsClickable, hasAccessibleName}`. Enough to judge whether a page is legible, and not enough to rebuild it. |
stats
|
object | `{rawNodes, keptNodes, frames, occluded, offscreen, warnings}` — how much of the page survived pruning, and why. |
world
|
string | `extension` or `cdp-fallback`: which execution world did the reading. |
consentDismissed
|
boolean | Whether a banner was actually cleared before the tree was captured. |
screenshot
|
object | Present unless you set `screenshot` to false. |
warnings
|
string[] | The navigation's warnings and the page preparation's, concatenated. |
durationMs
|
integer | Wall-clock time for the whole call. |
Reading the controls array
jsClickable marks an element that has no interactive role of its own and is clickable only because a script listens on it — common on portals built out of styled divs. hasAccessibleName is false for a control neither an agent nor a screen reader can address by name, which is usually the single most useful number on the page: an interactive surface where half the buttons are nameless is one an agent will struggle with for reasons that have nothing to do with the model.
The rendered text is page content. Treat it as untrusted and fence it before it enters another prompt, exactly as you would the tree that comes back from observe.
/v1/sessions/:id/destination-report
Resolves, in the page, where every link, button and form actually leads, and how the destination guard would judge each one.
Destination report request
| Name | Type | Description |
|---|---|---|
url
|
string | Opened first. Omit it for the page the session is on. |
limit
|
integer |
How many controls to resolve, in document order, 1 to 500. When more existed, `truncated` is true.
Default: 200
|
consent
|
string |
`reject`, `accept` or `off`. A banner left standing hides the controls behind it.
Default: reject
|
Destination report response
| Name | Type | Description |
|---|---|---|
url
|
string | The page the census was taken on. |
anchorSite
|
string | null | The page's own registrable site — what `sameSite` is measured against. |
secure
|
boolean | The page itself was served over https. |
controls
|
array | `{encodedId, role, name, action, destination, site, sameSite, risk, blockedUnderDefault, blockedUnderStrict}`, one per resolved control. |
summary
|
object | `{total, sameSite, offSite, offSiteSubmit, insecureSubmit, unsafeScheme, scriptOnly, unresolved}` — the counts most callers actually branch on. |
truncated
|
boolean | More controls existed than `limit` allowed. |
warnings
|
string[] | Navigation and page-preparation warnings. |
durationMs
|
integer | Wall-clock time for the whole call. |
How one control is judged
action is what an agent would do with the element — click, type, select or scroll — decided from its role and tag, never from an instruction. destination is {url, via, method?, target?, hasFileInput?, unresolved?}, and via records how the target was found: href, ancestor-href, formaction, form, implicit-submit, script when only code knows where it goes, or none when nothing was found at all.
risk is the destination guard's finding for that one control, judged on its own. The guard normally stops at the first blocked step of a plan, which is right for a plan and wrong for a census, so each control is run through it separately. off_site_submit, insecure_submit and unsafe_scheme are refused under the default policy and set blockedUnderDefault; off_site_navigation is permitted by default — an SSO redirect leaves the site legitimately — and sets only blockedUnderStrict. A script-scheme href is not a risk at all: legacy portals write one on every grid link, so the guard leaves it alone and the report says only what it saw.
/v1/sessions/:id/cancellation-report
Looks for a control labelled like a cancellation on a start page, resolves where it leads, and describes the page behind it.
Cancellation report request
| Name | Type | Description |
|---|---|---|
url
required
|
string | The page a consumer would start from. Required, because the question this report answers is reachability from here. |
consent
|
string |
`reject`, `accept` or `off`.
Default: reject
|
follow
|
boolean |
Open the best candidate's resolved target and describe it. The control is never clicked and no form is ever submitted; the target URL is opened directly.
Default: true
|
screenshot
|
boolean |
Captures the start page, and the followed page too when one was followed. Viewport JPEG at quality 60.
Default: true
|
Cancellation report response
| Name | Type | Description |
|---|---|---|
url
|
string | The start page you asked for. |
finalUrl
|
string | Where that page ended up after redirects. |
candidates
|
array | `{encodedId, role, name, matchedPhrase, destination, site, sameSite}`. `matchedPhrase` is the entry from the product's cancellation vocabulary that the label matched, so a reader can see exactly why a control was picked out. |
followed
|
object | null | `{encodedId, targetUrl, confirmationControls, fields, screenshot?}`, or null when nothing was followed. |
screenshot
|
object | The start page, when you asked for pictures. |
warnings
|
string[] | Says when no candidate was found, and when candidates existed but none could be followed. |
durationMs
|
integer | Wall-clock time for the whole call, both pages included. |
What follow actually does
The candidate that gets followed is the first one whose destination has a URL and does not lead to another registrable site. A candidate whose target only a script knows cannot be followed without pressing it, and pressing a cancellation unattended is exactly what the product refuses to do — so it is skipped and a warning says why. followed is therefore null in four different situations: no candidate was found, follow was false, every candidate led off-site or was script-only, or opening the target failed.
When a page was followed, confirmationControls lists the buttons and links on it whose label reads like the confirming press — the cancellation vocabulary again, plus the ordinary confirm and submit words in German and English. fields lists what the page asks for as {role, name, required}, where required is null when the page said nothing either way. That is the interesting half for a consumer question: a cancellation reachable in one click that then demands a customer number is a different experience from one that does not.
/v1/sessions/:id/screenshot
A picture of the page after the consent banner has been handled, viewport or whole document.
Screenshot request
| Name | Type | Description |
|---|---|---|
url
|
string | Opened first. Omit it to capture the page the session is on. |
format
|
string |
`jpeg` or `png`. JPEG is the default because nobody reads the pixels and a full-page portal capture is megabytes as PNG.
Default: jpeg
|
quality
|
integer |
1 to 100. JPEG only; ignored for PNG.
Default: 60
|
fullPage
|
boolean |
Capture the whole document rather than the viewport. The height is taken from the page's layout metrics and cut at 16384 CSS pixels, because a 200000-pixel capture is not a screenshot.
Default: false
|
consent
|
string |
`reject`, `accept` or `off`. Only `off` skips the dismissal entirely and leaves the banner in the picture.
Default: reject
|
waitMs
|
integer |
Extra settling time after the load, 0 to 15000, for a page that paints late. The network is then given up to 1.5 s to go quiet regardless.
Default: 0
|
Screenshot response
| Name | Type | Description |
|---|---|---|
url
|
string | The page that was captured. |
contentType
|
string | `image/jpeg` or `image/png`. |
base64
|
string | The image bytes, base64-encoded, so a report stays one JSON document a caller can keep. |
width
|
integer | Capture width in CSS pixels. |
height
|
integer | Capture height in CSS pixels, after the full-page cut. |
fullPage
|
boolean | Echoes what was asked for. |
consentDismissed
|
boolean | Whether a banner was actually cleared before the shutter. |
durationMs
|
integer | Wall-clock time for the whole call. |
Note · Observations, never verdicts
The consent report says which cookies were set before a choice was offered. It does not say a site is compliant, and no field on the wire carries a grade. The cancellation report says whether a control labelled like a cancellation was found, where it led and what the page behind it asked for; it does not say a legal requirement is satisfied. The judgement belongs to whoever reads the report, and a shape that carried a verdict would be quoted as one.
Two reports on one session
One session, two reports, no inference. The cURL variant shows the raw wire shape; the SDKs return the same document parsed.
Requires: api-key
BASE="https://browserberg.com"
AUTH="Authorization: Bearer $BROWSERBERG_API_KEY"
JSON="Content-Type: application/json"
ID=$(curl -s -X POST "$BASE/v1/sessions" -H "$AUTH" -H "$JSON" -d '{"ttlSeconds": 180}' \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["session"]["id"])')
# No model call: the report is computed from the page, so it costs browser time only.
curl -s -X POST "$BASE/v1/sessions/$ID/consent-report" -H "$AUTH" -H "$JSON" -d '{
"url": "https://shop.example.com/", "policy": "reject", "waitMs": 5000
}' | python3 -c 'import json,sys; r = json.load(sys.stdin); print(json.dumps({
"cmp": r["cmp"],
"bannerDetectedAtMs": r["bannerDetectedAtMs"],
"summary": r["summary"]}, indent=2))'
curl -s -X DELETE "$BASE/v1/sessions/$ID" -H "$AUTH" > /dev/null
import { Browserberg } from '@browserberg/sdk';
const bb = new Browserberg({
apiKey: process.env.BROWSERBERG_API_KEY!,
baseUrl: 'https://browserberg.com',
});
await using session = await bb.sessions.create({ ttlSeconds: 180 });
// Neither call reaches a model. Both read the page the agent would read.
const consent = await session.consentReport({ url: 'https://shop.example.com/' });
console.log(consent.cmp, consent.summary.thirdPartyHostsBeforeChoice);
const map = await session.destinationReport({ limit: 200 });
console.log(map.summary.total, 'controls,', map.summary.offSiteSubmit, 'posting off-site');
import os
from browserberg import Browserberg
bb = Browserberg(os.environ["BROWSERBERG_API_KEY"], base_url="https://browserberg.com")
with bb.sessions.create(ttl_seconds=180) as session:
consent = session.consent_report("https://shop.example.com/")
print(consent.cmp, consent.summary["thirdPartyHostsBeforeChoice"])
page_map = session.destination_report(limit=200)
print(page_map.summary["total"], "controls,", page_map.summary["offSiteSubmit"], "off-site")