POST
/v1/assertAPI keyRun assert[] against a URL. Credits: 3 + 1 per assert after the first (max 25 rules).
Why assert beats many visibility calls
Each rule reuses the same browser session. Prefer this for CI release gates, pricing-page checks, and badge verification. Single-selector occlusion detail remains on POST /v1/checks/visibility.
Request
| Name | Type | Required | Description |
|---|---|---|---|
url | string | required | Page to open. |
assert | object[] | required | 1–25 rules, each with selector + operators. |
scroll_into_view | boolean | optional | Scroll first match into view before visibility sampling (default true). |
ignore_selectors | string[] | optional | Overlays to ignore for not_covered. |
dismiss_cookie_banner / wait_for_selector / device / … | … | optional | Standard browse options. |
Operators per rule
| Field | Meaning |
|---|---|
exists | Element present / absent |
visible | CSS-visible + size (same sampling spirit as visibility) |
not_covered | Not occluded (elementsFromPoint) |
count / count_min / count_max | querySelectorAll length |
contains / text / matches | Text content (collapsed whitespace); matches = RegExp source |
href_contains | Anchor href substring |
attr | { name, value } exact attribute match |
soft | Failure reported but does not flip overall passed |
Selector-only rules imply exists: true.
Verify a pricing page
One load: CTA visible, price text contains 29, exactly one hero image.
RequestBody
{
"url": "https://example.com/pricing",
"dismiss_cookie_banner": true,
"assert": [
{ "selector": "#buy", "visible": true },
{ "selector": ".price", "contains": "29" },
{ "selector": "img.hero", "count": 1 }
]
}Response200 OK
{
"status": "success",
"url": "https://example.com/pricing",
"final_url": "https://example.com/pricing",
"passed": true,
"failed_count": 0,
"soft_failed_count": 0,
"results": [
{
"selector": "#buy",
"ok": true,
"soft": false,
"checks": [{ "name": "exists", "expected": true, "actual": true, "ok": true }, { "name": "visible", "expected": true, "actual": true, "ok": true }],
"evidence": { "count": 1, "text": "Buy now", "href": null, "visible": true, "in_viewport": true, "not_covered": true, "overlap_detected_by": null },
"error": null
},
{
"selector": ".price",
"ok": true,
"soft": false,
"checks": [{ "name": "exists", "expected": true, "actual": true, "ok": true }, { "name": "contains", "expected": "29", "actual": "€29 / mo", "ok": true }],
"evidence": { "count": 1, "text": "€29 / mo", "href": null, "visible": true, "in_viewport": null, "not_covered": null, "overlap_detected_by": null },
"error": null
},
{
"selector": "img.hero",
"ok": true,
"soft": false,
"checks": [{ "name": "count", "expected": 1, "actual": 1, "ok": true }],
"evidence": { "count": 1, "text": null, "href": null, "visible": null, "in_viewport": null, "not_covered": null, "overlap_detected_by": null },
"error": null
}
],
"response_time_ms": 1104,
"timestamp": "…"
}curl -X POST https://api.scout.macrocontent.dev/v1/assert \
-H "Authorization: Bearer sk_scout_…" \
-H "Content-Type: application/json" \
-d @assert.jsonResponse fields
passed— true when every non-softrule is okresults[]— per-ruleok,checks[](expected/actual),evidence,errorfailed_count/soft_failed_count