Monitoring

Monitor a page for changes and verify the webhook

Set up a scheduled check that fires only when the watched value actually moves — and write the HMAC verification correctly, including the raw-bytes detail everyone gets wrong.

A monitor is a URL, a cadence and a definition of what counts as "the value". Each check reads that value, hashes it, and compares it to last time; only a change fires a webhook. This guide covers picking the check type, choosing a cadence you can afford, and verifying the delivery — the last of which has one detail that silently breaks most first attempts.

Choose the check type first — it decides the price

There are two. check_type "observe" watches a CSS selector's text (or the page map if you give no selector): deterministic, cheap, and incapable of inventing a change — 1 credit per check. check_type "extract" watches LLM-extracted fields or a natural-language description: the only option when the value has no selector, and it bills the 5-credit extract floor on every check. Prefer observe whenever the value is addressable.

Choose a cadence with the arithmetic in front of you

interval_seconds has a 60-second floor, and every check is billed whether or not anything changed. A one-minute observe monitor is 1,440 checks a day; the same cadence on an extract check is 1,440 × the extract floor. Work out the daily number before you type 60.

Create the monitor

callback_url is required. callback_secret is not required and you should set it anyway — without it you cannot tell a real change webhook from anyone else who guesses your endpoint.

A selector-based monitorbash
curl -X POST https://twin-browser.com/api/v1/monitors \
  -H "Authorization: Bearer $TWIN_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "name": "competitor pricing",
    "check_type": "observe",
    "url": "https://competitor.example.com/pricing",
    "selector": ".price-table",
    "interval_seconds": 3600,
    "callback_url": "https://your.app/hooks/price",
    "callback_secret": "whsec_…"
  }'
# → 201 (the created monitor, secrets excluded)

What a change delivery looks like

The POST carries x-twin-event: monitor.changed and, when you set a secret, x-twin-signature: sha256=<hex>. The body reports both hashes plus an excerpt of the new value, so your handler can act without having stored the previous state itself.

The webhook bodyjson
{
  "event": "monitor.changed",
  "monitorId": "…",
  "name": "competitor pricing",
  "checkType": "observe",
  "url": "https://competitor.example.com/pricing",
  "changedAt": "2026-09-02T09:14:22.518Z",
  "previous": { "hash": "9f2c…" },
  "current":  { "hash": "1ab7…", "value": "…excerpt of the new value…" }
}

Verifying the signature — the part that goes wrong

Compute an HMAC-SHA256 over the RAW request body with your secret, hex-encode it, and compare in constant time against the hex after `sha256=`. The mistake is parsing the JSON and re-serializing it before hashing: that changes the bytes — key order, whitespace, number formatting — and the signature will never match. Read the raw body first, verify, then parse.

Verify in Nodejs
import { createHmac, timingSafeEqual } from 'node:crypto';

export async function handler(req) {
  // RAW bytes first. Do NOT JSON.parse and re-stringify before hashing.
  const raw = await req.text();
  const header = req.headers.get('x-twin-signature') ?? '';

  const expected = 'sha256=' +
    createHmac('sha256', process.env.MONITOR_SECRET).update(raw).digest('hex');

  const a = Buffer.from(header);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return new Response('bad signature', { status: 401 });
  }

  const payload = JSON.parse(raw);   // safe to parse now
  // … act on payload.current.value
  return new Response('ok');
}

When it goes quiet

A monitor that is erroring looks exactly like a page that never changes — silence. GET /api/v1/monitors/{id}/history returns recent checks with changed / unchanged / error and value excerpts, paginated up to 200. Check it when a monitor has been suspiciously calm. A monitor whose tenant runs out of credits is paused rather than silently dropped, and you keep the definition and the history.

Keep going

The capabilities behind it

Common questions

Do I get a webhook on every check?
No — only on a change. The check hashes the normalized value and compares it to the previous hash; an unchanged check appears in the history and does nothing else. That is what makes a tight cadence survivable on your side, even though every check is billed.
Why does my signature never match?
Almost certainly because you are hashing re-serialized JSON. The HMAC is over the RAW body bytes. Read the body as text, verify, and only then parse it.
What is the minimum interval?
60 seconds. It is a hard floor, and at that cadence an observe monitor is 1,440 billed checks a day — worth doing the arithmetic on before you set it.
What happens if my balance runs out?
The monitor is paused rather than silently skipped, and the pause is recorded. Top up and re-enable it; the definition and its check history survive.

Delegate the work. Keep the decision.

Hand off a real task, set the guardrails, and let repeated work compile into a skill that replays deterministically at near-zero cost.