# Assertions and validation rules

For Website/HTTPS and API monitors, **Assertion mode** replaces the classic status-code and keyword fields with a
list of rules written in HostTracker's assertion language. Each rule is one line; the check passes only when
**every** rule passes. Use it when "the page answered 200" is not enough: an API must say `"status":"ok"`, a page
must contain its checkout button, a response must arrive within 2 seconds, a redirect must stay on your domain.

## Settings reference

| Setting (app label) | API field (`settings.*`) | Type / allowed values | Default | Plan limits | What it does for you |
|---|---|---|---|---|---|
| **Assertion mode** | `assertMode` | Boolean | Off | Available on every plan that includes the monitor type | Makes the rule list the response verdict. |
| The rule list | `assertsSource` (write) | Text, one rule per line, optional `label: rule` | None | Up to 20 rules per monitor; your plan may allow fewer | The rules, as you would type them. Parsed and checked on save. |
| - | `asserts` (read/write) | Array of rule objects `{ name, sub, op, not, val, vals, valSub, valT, nocase }` | None | Same | The compiled form. Send either `asserts` or `assertsSource`, not both. |
| - | `assertsText` (read-only) | Array of strings | - | - | Your rules rendered back as text. |

## How a rule reads

A rule is `subject [not] predicate`, optionally labelled and optionally followed by `nocase`:

```text
status isOk
"json ok": body.json.path("$.status") eq "ok"
time lt 2s
body not contains "Error"
header("Content-Type") isJson
```

- Strings need double quotes; numbers can carry units (`2s`, `500ms`, `10kb`, `1mb`).
- `not` goes after the subject (`body not contains "x"`); a leading `not` does not parse.
- A rule that fails names itself in the alert - short labels tell you **which** expectation broke.
- There is no `or` between rules; `in [...]`, `containsAny [...]` and ranges such as `in [200..299]` cover most
  cases.

## What you can check

| Area | Example | Catches |
|---|---|---|
| Status | `status isOk`, `status in [200, 301, 302]`, `status not in [500..599]` | Errors, or a status outside what you allow |
| Body text | `body contains "Add to cart"`, `body containsAll ["Checkout", "Total"]`, `body matches "Order #[0-9]{6}"` | A 200 page that lost its content - the failure a status check cannot see |
| Body size | `body.size gt 1kb` | A blank or truncated page |
| JSON | `body.json.path("$.status") eq "ok"`, `body.json.path("$.items").count ge 1`, `body isJson` | The API's own verdict, empty results, an HTML error page instead of JSON |
| Headers | `header("Content-Type") isJson`, `header("Cache-Control") matches "no-store"` | Wrong content type or caching |
| Timing | `time lt 2s`, `time.connect lt 300ms`, `time.tls lt 500ms`, `time.dns lt 200ms`, `time.head lt 1s` | Slowness, and which stage causes it (DNS is not part of `time`) |
| Redirects | `redirects.count le 2`, `url.scheme eq "https"`, `url.host eq url.original.host`, `requests[0].status eq 301` | Chains that grow, leave your domain or change type |
| DNS | `dns.ips.count ge 2`, `dns.ips contains "203.0.113.10"` | Losing redundancy, or an address change |
| Certificate | `cert.days.left gt 14` | A certificate close to expiry, with time to act |
| Change detection | `previous.body.hash eq body.hash`, `delta(body.json.path("$.orders")) lt 100` | A page that changed, or a value that jumped since the last check |

The full grammar, every subject and predicate, and a recipe catalogue are in the
[assertion language reference](/reference/assert-language/).

## Set it up in the app

1. On **Sites**, open a Website/HTTPS or API monitor and expand **Response Validation**.
2. Switch on **Assertion mode**. The classic keyword and status fields are hidden, and the list starts with a
   status rule.
3. Add rules with the rule editor, or pick one under **Examples - click to add:**.
4. **Save**. Rules that do not compile are refused line by line with the reason.

Switching the mode off before saving brings the classic settings back. Saving with the mode off removes the rules
from the monitor.

## Do it with the API or MCP

```bash
# Scope monitor:write
curl -X PATCH "https://api2.host-tracker.com/monitor/$MONITOR_ID" \
  -H "Authorization: Bearer $HT_TOKEN" -H "Content-Type: application/json" \
  -d '{ "settings": {
        "assertMode": true,
        "assertsSource": "status isOk\n\"json ok\": body.json.path(\"$.status\") eq \"ok\"\ntime lt 2s"
      } }'
```

The response shows the stored rules as `asserts` (compiled) and `assertsText` (readable). While `assertMode` is
on, `keywords`, `keywordMode`, `ignoredStatuses`, `errorStatuses`, `errorOnRedirect` and `preset` are refused -
clear them in the same request if the monitor had them. MCP: **`update_monitor`** / **`create_monitor`** with
`settingsJson` carrying the same members.

## What happens next

Every check evaluates all rules and reports every failed one in the result (`AssertError`). A connection
failure - DNS, connect, TLS, timeout, a redirect loop - still fails the check whatever the rules say. A failed
check is [re-checked](/monitors/down-detection/) like any other before the monitor turns Down.

## Limits and gotchas

- **Rule count.** At most 20 rules per monitor; your plan may set a lower number. More is refused with
  `403 package_limit` ("Your package allows up to N assertion rules per check").
- **Change detection is a plan feature.** Rules using `previous.`, `delta(...)` or `delta2(...)` need the
  stateful-assertions entitlement ("Stateful assertions are not available on your package" otherwise). Their
  first check has nothing to compare against, so it counts as a pass marked "warming" (`delta2` needs three
  checks).
- **Large pages.** Only the downloaded part of the body (the
  [max response size](/monitors/advanced/response-limits/), 1 MB by default) is judged. A rule whose outcome
  depends on **not** finding something in a cut-off body - `body contains "x"` failing, or
  `body not contains "x"` passing - cannot be proven, and the check fails with `ContentTooLarge` instead. Raise the
  max response size or match text nearer the top.
- **No implicit "any" or "all".** A list compared with a single value (`body.json.path("$.items[*].price") gt 0`)
  does not pass - say which item you mean with `.first` / `.last`, count with `.count`, or test every item with
  `in [...]`.
- `redirects.*` rules need **Follow redirects** on; **Redirect is error** cannot be combined with assertion mode.
- Through the API, rules sent with `assertMode: false` are evaluated in addition to the classic status and keyword
  fields.

## Related

- [Assertion language reference](/reference/assert-language/)
- [HTTP request configuration](/monitors/advanced/http-request-config/)
- [Timeouts and response size](/monitors/advanced/response-limits/)
- [Plan limits](/reference/plan-limits/)
