Page content monitoring (Content monitor)
Content monitor watches the content, not the resource availability: Notifly fetches the specified URL every N seconds, extracts the value of interest from the response and compares it with the previous one. As soon as the value changes — an alert is sent. This is an active monitor (Notifly initiates the check), but it answers the question “what changed on the page” rather than “is the site up”.
Typical use cases:
- Price — track the price of a product on a store page or a competitor’s site.
- Status / availability — “in stock” ↔ “out of stock”, order status, service status.
- Competitor content — a new blog post appeared, the price list changed, the changelog updated.
- JSON API — the value of a field in a public JSON response (rate, balance, version).
Extraction modes
Section titled “Extraction modes”What to compare is defined by the mode (mode). Five modes are supported:
mode | What is extracted | Field selector |
|---|---|---|
hash | SHA-256 of the entire response body — alert on any page change | not needed |
json | Value by JSONPath from a JSON response | required — JSONPath, e.g. $.price or $.items.0.status |
text | First match of a regular expression (Go syntax) in the response text | regex; if empty — behaves like hash |
css | Text of the first element matched by a CSS selector | required — valid CSS selector |
xpath | Text of the first node matched by XPath (attributes too: //a/@href) | required — valid XPath expression |
The check logic is the same for all modes: extract the value → compare with the
snapshot from the previous check → if different, send an alert and remember the new
value. The first check just stores the current value and does not send an alert.
Whitespace in element text (css/xpath) is collapsed, so line breaks in the markup
don’t produce false changes.
If a value cannot be extracted (the selector found nothing, the element is empty,
the JSONPath is missing, the regex didn’t match), that is not a “change” but a
check error: the monitor goes down (see Notifications).
hash reacts to any changed byte — including counters, tokens and timestamps in the
markup. For “live” pages it’s more reliable to watch a specific fragment with
css/xpath.
Multiple rules in one monitor
Section titled “Multiple rules in one monitor”Instead of a single mode+selector you can provide an array of rules (rules) —
up to 20 rules per monitor. Each rule is { "mode", "selector", "label" },
where label is an optional human-readable name (it also appears in the alert text).
An alert is triggered if any rule changed. This is convenient when you need to
monitor several values on the same page (e.g., price + availability + rating).
"rules": [ { "mode": "css", "selector": ".product-price", "label": "Price" }, { "mode": "css", "selector": ".availability", "label": "Availability" }, { "mode": "text", "selector": "Discount\\s+\\d+%", "label": "Discount" }]If rules is set, top-level fields mode/selector are ignored. If rules
is empty — the single mode (mode+selector) works, preserved for backward
compatibility.
The snapshot is stored per rule: reordering rules causes no false alerts, and a new or modified rule only records its value on the first check.
JavaScript pages: useBrowser
Section titled “JavaScript pages: useBrowser”By default the page is loaded via a plain HTTP GET. If the needed value
is injected by scripts in the browser (SPA, lazy-loaded price, etc.),
enable the flag useBrowser: true — then the page is rendered in headless
Chromium and values are extracted from the final DOM after JS execution (text —
from the page’s visible text, hash — from the rendered HTML).
If a browser-mode check fails, Notifly takes a screenshot of the page (when
screenshot storage is configured on the server) and attaches it to the error alert:
its key arrives in the message extras under notifly::screenshot, the image is
served by GET /browser-shot/<key> and a preview is shown on the message card in
the admin UI. Change alerts are sent without a screenshot by default. Frames not
attached to an alert are not kept; access is limited to the monitor owner; frames
live within the plan’s storage quota and are deleted
together with the monitor.
Notifications
Section titled “Notifications”| Event | When | Text |
|---|---|---|
| Change | The value of at least one rule differs from the previous check’s snapshot — on every change | alertMessage + a list of “was → now” changes (values truncated to 100 characters) |
| Check error | The page failed to load (network, timeout, HTTP code ≥ 400) or a rule extracted no value; once per outage | Ошибка проверки: <reason> (“Check error”) |
| Check recovered | First successful check after down — only if an error alert was sent | ✅ Проверка восстановлена (“Check recovered”) + what the error was |
All three are sent with the alertTitle title and alertPriority priority. The
change text is built as Markdown, so enable alertMessageMarkdown: true (the admin UI
does this automatically). The service part of the text is currently generated in
Russian. Example alert:
Product #42 price changed!
🔔 Обнаружено изменение
**Price**
было: ~~1 299 ₽~~
стало: **1 199 ₽**If a rule has no label, the text says “правило N” (“rule N”).
Monitor statuses: pending (created or resumed), up, down (the last check was
an error) and paused. A content monitor has no “N failures in a row” threshold:
the very first error puts it down.
- No spam. The monitor reports an error once: the alert time is stored in
alertedAt, and while the monitor isdownno repeated messages are sent. Re-arm: if you change check parameters viaPUT /content-monitor/:id(URL, mode, selector, rules,useBrowser, interval, timeout), the mark is cleared and a check runs right away — if the error persists, a fresh alert arrives. - A timeout resets the snapshot. After a timeout error the first successful check records the values anew and does not report a change. With other errors the snapshot is kept, so a change that happened during the outage will be noticed.
- Notifly infrastructure failures (e.g. the browser service is unavailable) are not the site’s fault: no alert, the status doesn’t change, the quota is refunded, and the check is retried in 2 minutes.
- Maintenance windows: during a window checks run and are recorded in history, but the error alert is deferred until the window ends, and detected changes are neither reported nor accepted as the new snapshot — after the window the page is compared with the pre-maintenance snapshot. A change that outlives the window will be noticed; a temporary “under maintenance” stub will not.
- Quiet hours suppress escalation to external channels for
messages with priority below
breakthroughPriority(8 by default). - The channel’s status page: going
down(a check error, not a change) opens an auto-incident, recovery closes it. - Each check is one event of the daily quota; when it is exhausted, checks are postponed until the next day.
Selector testing and selection
Section titled “Selector testing and selection”Before creating a monitor, it is convenient to try and validate a rule on the fly — there are two endpoints that do not require creating a monitor. Both load the page the same way as the live monitor and apply the rule with the same code. The loaded page is cached for 90 seconds: repeated tests on the same URL don’t reload it.
Test a rule — POST /content-monitor/test-rule
Section titled “Test a rule — POST /content-monitor/test-rule”Loads the page and applies a single rule, returning the extracted value or a clear error:
curl -X POST "$NOTIFLY_URL/content-monitor/test-rule" \ -H "Content-Type: application/json" \ -H "X-Notifly-Key: <api-token>" \ -d '{ "url": "https://example.com/product/42", "useBrowser": false, "mode": "css", "selector": ".product-price" }'# → {"ok":true,"value":"1 299 ₽"}Response (always 200): ok (boolean), value (extracted value, truncated to
200 characters; for hash — a short page fingerprint), error (error text if the
page failed to load or the rule didn’t work) and note (diagnostics, e.g. about
browser mode falling back to GET).
Suggest a selector using AI — POST /content-monitor/suggest-selector
Section titled “Suggest a selector using AI — POST /content-monitor/suggest-selector”Notifly loads the page, and the AI uses your description to decide what to track
and to find its current value on the page; from that value the server itself builds
a stable CSS/XPath selector, and if that fails, the AI synthesizes a selector from
a page fragment (with verification and retries). For a JSON response the json mode
is chosen. The result is always verified against the real page content, so the
sampleValue in the response is the actual current value:
curl -X POST "$NOTIFLY_URL/content-monitor/suggest-selector" \ -H "Content-Type: application/json" \ -H "X-Notifly-Key: <api-token>" \ -d '{ "url": "https://example.com/product/42", "useBrowser": false, "prompt": "track the product price" }'The response includes: mode, selector, label, sampleValue (current value on the page),
reason (why), confidence (high/medium/low, when rated), alternatives
(other candidates with their sampleValue), fallback (true if the selector was
chosen heuristically — the h1/title heading or the page hash — without AI or after
it failed) and note. The prompt field is up to 500 characters. A request like
“monitor the whole page” immediately returns hash mode without calling the AI.
If the page fails to load, the response is 502.
Creating a monitor
Section titled “Creating a monitor”Via the dashboard
Section titled “Via the dashboard”- Open app.notifly.ru → Monitors → Create → “Page content changed” (or the Content tab → “Add content monitor”).
- Enter the URL and enable “Browser mode” if needed. In the “AI selector picker” block describe in words what to watch, click “Generate selector” and “Add to rules” — or set up the rules manually.
- Use a rule’s “Test” button to make sure the extracted value is exactly what you need.
- Set the interval, timeout, channel and alert text (Markdown is supported) — and save.
The form doesn’t configure the alert title and priority (priority 5) — set them via the REST API or MCP. Note that saving the monitor from the form resets the priority to 5.
Via the REST API — POST /content-monitor
Section titled “Via the REST API — POST /content-monitor”Single mode (monitoring one CSS value):
curl -X POST "$NOTIFLY_URL/content-monitor" \ -H "Content-Type: application/json" \ -H "X-Notifly-Key: <api-token>" \ -d '{ "appid": 12345, "name": "Product #42 price", "url": "https://example.com/product/42", "mode": "css", "selector": ".product-price", "useBrowser": false, "intervalSec": 3600, "timeoutSec": 15, "alertMessage": "Product #42 price changed!", "alertMessageMarkdown": true, "alertPriority": 7 }'Multiple mode (several rules on one page):
curl -X POST "$NOTIFLY_URL/content-monitor" \ -H "Content-Type: application/json" \ -H "X-Notifly-Key: <api-token>" \ -d '{ "appid": 12345, "name": "Product card", "url": "https://example.com/product/42", "rules": [ { "mode": "css", "selector": ".product-price", "label": "Price" }, { "mode": "css", "selector": ".availability", "label": "Availability" } ], "intervalSec": 3600, "alertMessage": "The product card content changed.", "alertMessageMarkdown": true, "alertPriority": 5 }'Monitoring a field in a JSON API:
curl -X POST "$NOTIFLY_URL/content-monitor" \ -H "Content-Type: application/json" \ -H "X-Notifly-Key: <api-token>" \ -d '{ "appid": 12345, "name": "Rate from API", "url": "https://api.example.com/rates", "mode": "json", "selector": "$.usd.value", "intervalSec": 600, "alertMessage": "USD rate changed.", "alertMessageMarkdown": true }'Request fields
Section titled “Request fields”| Field | Type | Required | Description / constraints |
|---|---|---|---|
appid | uint | yes | ID of the channel to send the alert to |
name | string | yes | Monitor name, 1–200 characters |
url | string | yes | Valid http(s) URL pointing to a public address |
mode | string | yes* | Single mode: hash / json / text / css / xpath |
selector | string | depends | JSONPath for json, regex for text, CSS for css, XPath for xpath; required for json/css/xpath |
rules | array | yes* | Array of rules {mode, selector, label}, up to 20; alternative to mode+selector |
useBrowser | bool | no | true — render via headless Chromium |
intervalSec | uint | yes | Check interval, 60–86400 seconds |
timeoutSec | uint | no | Load timeout, default 15, maximum 60 |
alertMessage | string | yes | Change notification text, 1–2000 characters |
alertTitle | string | no | Title of all the monitor’s notifications |
alertMessageMarkdown | bool | no | Render notifications as Markdown (true recommended) |
alertPriority | int | no | Notification priority, 0–10 |
* You must set either rules (multiple mode) or mode+selector (single mode).
If rules is non-empty — mode/selector are ignored. CSS selectors, XPath
expressions and regexes are syntax-checked on save (400 on error).
Lifecycle
Section titled “Lifecycle”A new monitor is created with status pending and is checked almost immediately
(nextCheckAt = “now”). After changing check parameters (URL, mode, selector,
rules, useBrowser, interval, timeout) the check is rescheduled for “now”; editing
the name, alert title/message or priority does not shift the schedule.
PUT /content-monitor/:id accepts the same full set of fields (except appid).
POST /content-monitor/:id/pause— pause (checks are not performed;nextCheckAtis moved far into the future).POST /content-monitor/:id/resume— resume (status becomespendingagain, check is scheduled for “now”).
The monitor record stores lastValue (snapshot of the last successful check — a JSON
object with the rule values), lastError (last load/extraction error), lastCheckAt,
nextCheckAt and alertedAt. Check history with extracted values and detected
changes — GET /monitor-history/content_monitor/:id (see
Active monitors) and the “History” tab in the
monitor’s edit dialog.
Limits
Section titled “Limits”- Check interval: 60–86400 seconds (1 minute to 24 hours).
- Load timeout: default 15 s, maximum 60 s.
- Up to 20 rules per monitor.
- Alert priority: 0–10;
namelength — 1–200,alertMessage— 1–2000 characters. - The page is loaded with a
GETrequest without custom headers or body; up to 1 MB of the response is read. An HTTP code ≥ 400 is a check error. - Requests originate from the cloud: the URL must be reachable from the public internet; private-network, loopback and link-local addresses are rejected.
- Browser mode (
useBrowser) works only if the browser service is configured on the server.
REST API
Section titled “REST API”Authorization — the X-Notifly-Key header with an API token (or a
device token). Create, update, delete, pause and resume require write access for an
API token; listing, test-rule and suggest-selector only need read. A token
restricted to certain channels sees and changes only the monitors of those channels.
Channel share tokens are not allowed to manage monitors (403).
| Method | Path | Description |
|---|---|---|
GET | /content-monitor | List of the user’s content monitors |
POST | /content-monitor | Create a monitor |
POST | /content-monitor/suggest-selector | Suggest mode + selector with AI by description |
POST | /content-monitor/test-rule | Apply one rule to a page (dry-run), return value or error |
PUT | /content-monitor/:id | Update settings (cannot change appid) |
DELETE | /content-monitor/:id | Delete a monitor (together with its screenshots) |
POST | /content-monitor/:id/pause | Pause checks |
POST | /content-monitor/:id/resume | Resume checks |
GET | /browser-shot/<key> | Screenshot from alert extras (monitor owner only) |
Content monitors can also be created and rules tested in plain words via MCP — for example, “watch the price on https://example.com/product/42 every hour”.
See also: Active monitors, HTTP monitors, Workflow monitors, Browser-workflow.