Skip to main content
Context.dev’s Monitors API watches a page, a sitemap, or extracted structured data on a schedule you set, compares each run against a baseline, and notifies you (via signed webhook or the API) only when something actually changes. You create a monitor once; Context.dev handles the crawling, diffing, judging, and retrying in the background.

Integrate Context.dev's Monitors API in your app

Open in Cursor

Prerequisites

  • A Context.dev API key. Sign up at context.dev/signup, copy the key from the dashboard (prefix ctxt_secret_), and export it:
    export CONTEXT_DEV_API_KEY="ctxt_secret_..."
    
  • An SDK (optional). Install for your language, or skip the install and call directly with curl:
    npm install context.dev
    
    pip install context.dev
    
    gem install context.dev
    
    go get github.com/context-dot-dev/context-go-sdk
    
    composer require context-dev/context-dev-php
    

Create a monitor

A monitor is defined by four things: a target (what to watch), a change_detection strategy (how to compare), a schedule (how often), and an optional webhook (where to notify). This creates a monitor that checks a pricing page every 6 hours:
curl -X POST https://api.context.dev/v1/monitors \
  -H "Authorization: Bearer $CONTEXT_DEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme pricing page",
    "target": { "type": "page", "url": "https://acme.com/pricing" },
    "change_detection": { "type": "exact" },
    "schedule": { "type": "interval", "frequency": 6, "unit": "hours" },
    "webhook": { "url": "https://example.com/webhook" }
  }'
import ContextDev from "context.dev";

const client = new ContextDev({ apiKey: process.env.CONTEXT_DEV_API_KEY });

const monitor = await client.monitors.create({
  name: "Acme pricing page",
  target: { type: "page", url: "https://acme.com/pricing" },
  change_detection: { type: "exact" },
  schedule: { type: "interval", frequency: 6, unit: "hours" },
  webhook: { url: "https://example.com/webhook" },
});

console.log(monitor.id, monitor.webhook?.secret);
import os
from context.dev import ContextDev

client = ContextDev(api_key=os.environ["CONTEXT_DEV_API_KEY"])

monitor = client.monitors.create(
    name="Acme pricing page",
    target={"type": "page", "url": "https://acme.com/pricing"},
    change_detection={"type": "exact"},
    schedule={"type": "interval", "frequency": 6, "unit": "hours"},
    webhook={"url": "https://example.com/webhook"},
)

print(monitor.id, monitor.webhook.secret)
require "context_dev"

client = ContextDev::Client.new(api_key: ENV.fetch("CONTEXT_DEV_API_KEY"))

monitor = client.monitors.create(
  name: "Acme pricing page",
  target: {type: :page, url: "https://acme.com/pricing"},
  change_detection: {type: :exact},
  schedule: {type: :interval, frequency: 6, unit: :hours},
  webhook: {url: "https://example.com/webhook"}
)

puts monitor.id
package main

import (
    "context"
    "fmt"
    "os"

    contextdev "github.com/context-dot-dev/context-go-sdk"
    "github.com/context-dot-dev/context-go-sdk/option"
)

func main() {
    client := contextdev.NewClient(
        option.WithAPIKey(os.Getenv("CONTEXT_DEV_API_KEY")),
    )

    monitor, err := client.Monitors.New(context.TODO(), contextdev.MonitorNewParams{
        Name: "Acme pricing page",
        Target: contextdev.MonitorNewParamsTargetUnion{
            OfPage: &contextdev.MonitorNewParamsTargetPage{
                URL: "https://acme.com/pricing",
            },
        },
        ChangeDetection: contextdev.MonitorNewParamsChangeDetectionUnion{
            OfExact: &contextdev.MonitorNewParamsChangeDetectionExact{},
        },
        Schedule: contextdev.MonitorNewParamsSchedule{
            Type:      "interval",
            Frequency: 6,
            Unit:      "hours",
        },
        Webhook: contextdev.MonitorNewParamsWebhook{
            URL: "https://example.com/webhook",
        },
    })
    if err != nil {
        panic(err)
    }

    fmt.Println(monitor.ID)
}
<?php

use ContextDev\Client;

$client = new Client(apiKey: getenv('CONTEXT_DEV_API_KEY'));

$monitor = $client->monitors->create(
    name: 'Acme pricing page',
    target: ['type' => 'page', 'url' => 'https://acme.com/pricing'],
    changeDetection: ['type' => 'exact'],
    schedule: ['type' => 'interval', 'frequency' => 6, 'unit' => 'hours'],
    webhook: ['url' => 'https://example.com/webhook'],
);

echo $monitor->id;
Management calls are free; you pay per run: 1 credit for page & sitemap checks, 10 for semantic extract The monitor runs immediately after creation to capture its initial baseline: the snapshot every later run is compared against. The response includes the generated webhook signing secret; store it, you’ll need it to verify deliveries:
sample response
{
  "mode": "web",
  "id": "mon_123",
  "name": "Acme pricing page",
  "target": {
    "type": "page",
    "url": "https://acme.com/pricing",
    "normalize_whitespace": true
  },
  "change_detection": { "type": "exact" },
  "schedule": { "type": "interval", "frequency": 6, "unit": "hours" },
  "webhook": {
    "url": "https://example.com/webhook",
    "secret": "whsec_8f3a..."
  },
  "status": "active",
  "last_run_at": null,
  "last_change_at": null,
  "next_run_at": "2026-07-09T18:00:00Z",
  "last_error": null,
  "tags": [],
  "created_at": "2026-07-09T12:00:00Z",
  "updated_at": "2026-07-09T12:00:00Z"
}

Request parameters

ParameterTypeDescription
namestringRequired. Display name, 1–200 characters.
targetobjectRequired. What to watch: a page, sitemap, or extract target. See Pick a target.
change_detectionobjectRequired. { "type": "exact" } for literal diffs, or { "type": "semantic", "confidence_threshold": 0.75 } for meaning-level changes.
scheduleobjectRequired. { "type": "interval", "frequency": 6, "unit": "hours" }. Units: minutes, hours, days. The total interval must be between 10 minutes and 1 year.
webhookobject{ "url": "https://..." } to call when a change is detected. The API generates the signing secret; it can’t be set by clients.
tagsstring[]Up to 20 tags for grouping and filtering monitors and their changes.

Pick a target

Each target type pairs with a specific detection strategy. Supported combinations: page + exact, sitemap + exact, and extract + semantic.

Page: did this page’s text change?

Watches a single URL and diffs the visible page text. Whitespace is normalized by default (normalize_whitespace: true).
{
  "target": { "type": "page", "url": "https://acme.com/pricing" },
  "change_detection": { "type": "exact" }
}

Sitemap: were URLs added or removed?

Watches a sitemap for URL additions and removals, ideal for catching new blog posts, product pages, or docs. URLs are normalized and scoped to the monitored site and its subdomains; on a detected difference the sitemap is re-fetched within the same run and only URLs both observations agree on are reported, suppressing transient crawl flaps.
{
  "target": {
    "type": "sitemap",
    "url": "https://acme.com/sitemap.xml",
    "include": ["/blog/*"],
    "exclude": ["/legal/*"],
    "max_urls": 5000
  },
  "change_detection": { "type": "exact" }
}
include / exclude accept URL path patterns, and max_urls caps tracking at up to 10,000 URLs (default 5,000).

Extract: did the data I care about change?

Watches the meaning of a site, not its markup. A crawl guided by your instructions (and optional JSON schema, the same shape the Extract API uses) selects up to max_pages relevant pages; each run re-checks exactly those pages and judges confirmed changes against your instructions, so a reworded paragraph doesn’t fire, but a new pricing tier does. The schema does three things: it steers which pages get selected for tracking, it gives the change judge extra context on which changes matter (alongside your instructions), and it defines the shape of the data snapshot on the monitor’s baseline (refreshed by the re-discovery crawl, at most about once a day). It is not a response format for alerts — change events and webhook payloads always contain diffs, summaries, and evidence excerpts, never data shaped by your schema. Skip it and a general summary + key-points schema is used.
{
  "target": {
    "type": "extract",
    "url": "https://acme.com",
    "instructions": "Extract every pricing plan with its monthly price and included limits.",
    "schema": {
      "type": "object",
      "properties": {
        "plans": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "price": { "type": "string" }
            }
          }
        }
      }
    },
    "max_pages": 10
  },
  "change_detection": { "type": "semantic", "confidence_threshold": 0.75 }
}
Raise confidence_threshold (0–1, default 0.75) to only get notified about changes the judge is more certain matter. The tracked page set is refreshed by a periodic re-discovery crawl (at most about once a day).

Receive webhooks

When a run detects a change, Context.dev POSTs a change.detected event to your webhook URL:
webhook payload
{
  "event": "change.detected",
  "id": "evt_123",
  "created_at": "2026-07-09T18:00:12Z",
  "data": {
    "change": {
      "mode": "web",
      "id": "chg_123",
      "monitor_id": "mon_123",
      "target_type": "page",
      "change_detection_type": "exact",
      "title": "Acme pricing page changed",
      "summary": "The visible text on the page changed.",
      "detected_at": "2026-07-09T18:00:10Z",
      "url": "https://acme.com/pricing"
    }
  }
}
Every delivery includes an X-Context-Signature: t=<unix>,v1=<hmac> header, where the HMAC is SHA-256 over "{t}.{rawRequestBody}" keyed by your monitor’s webhook secret. Recompute it with a constant-time compare and reject stale timestamps to prevent replays:
import crypto from "node:crypto";

function verifyWebhook(rawBody: string, signatureHeader: string, secret: string): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((pair) => pair.split("=")),
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest();

  const isFresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  const received = Buffer.from(parts.v1 ?? "", "hex");
  return (
    isFresh &&
    received.length === expected.length &&
    crypto.timingSafeEqual(expected, received)
  );
}
import hashlib
import hmac
import time

def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    parts = dict(pair.split("=", 1) for pair in signature_header.split(","))
    message = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()

    is_fresh = abs(time.time() - int(parts["t"])) < 300
    return is_fresh and hmac.compare_digest(expected, parts["v1"])
The webhook payload carries a lightweight change summary. Fetch the full diff, added/removed URLs, or semantic evidence with GET /monitors/changes/{change_id} using the data.change.id from the payload.

Read changes over the API

No webhook? Poll instead. List a monitor’s detected changes (paginated via cursor, filterable by since / until / tag), then fetch full details for any change: text diff for page monitors, added_urls / removed_urls for sitemaps, and evidence with before/after snapshots plus confidence and importance for semantic monitors.
curl "https://api.context.dev/v1/monitors/mon_123/changes?limit=20" \
  -H "Authorization: Bearer $CONTEXT_DEV_API_KEY"

curl https://api.context.dev/v1/monitors/changes/chg_123 \
  -H "Authorization: Bearer $CONTEXT_DEV_API_KEY"
const changes = await client.monitors.listChanges("mon_123", { limit: 20 });

for (const change of changes.data) {
  const detail = await client.monitors.retrieveChange(change.id);
  console.log(detail.title, detail.diff);
}
changes = client.monitors.list_changes(monitor_id="mon_123", limit=20)

for change in changes.data:
    detail = client.monitors.retrieve_change(change.id)
    print(detail.title, detail.diff)
changes = client.monitors.list_changes("mon_123", limit: 20)

changes.data.each do |change|
  detail = client.monitors.retrieve_change(change.id)
  puts detail.title
end
changes, err := client.Monitors.ListChanges(
    context.TODO(),
    "mon_123",
    contextdev.MonitorListChangesParams{},
)
if err != nil {
    panic(err)
}

for _, change := range changes.Data {
    detail, err := client.Monitors.GetChange(context.TODO(), change.ID)
    if err != nil {
        panic(err)
    }
    fmt.Println(detail.Title)
}
<?php

$changes = $client->monitors->listChanges('mon_123', limit: 20);

foreach ($changes->data as $change) {
    $detail = $client->monitors->retrieveChange($change->id);
    echo $detail->title;
}
There’s also an account-wide feed across all monitors, GET /monitors/changes (client.monitors.listAccountChanges()), and the same pair for run history: GET /monitors/{monitor_id}/runs and GET /monitors/runs.

Run, pause, and manage

Trigger an off-schedule check (queued and processed asynchronously), pause and resume, or delete:
curl -X POST https://api.context.dev/v1/monitors/mon_123/run \
  -H "Authorization: Bearer $CONTEXT_DEV_API_KEY"

curl -X PATCH https://api.context.dev/v1/monitors/mon_123 \
  -H "Authorization: Bearer $CONTEXT_DEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "paused" }'

curl -X DELETE https://api.context.dev/v1/monitors/mon_123 \
  -H "Authorization: Bearer $CONTEXT_DEV_API_KEY"
await client.monitors.run("mon_123");

await client.monitors.update("mon_123", { status: "paused" });

await client.monitors.delete("mon_123");
client.monitors.run("mon_123")

client.monitors.update(monitor_id="mon_123", status="paused")

client.monitors.delete("mon_123")
Two behaviors worth knowing:
  • Updating target or change_detection resets the baseline. The next run re-captures it instead of reporting a spurious change. Unsupported target/detection combinations are rejected.
  • Failures don’t kill monitors. A failed run flips status to failed (with last_error populated), but the monitor keeps running on schedule and flips back to active on the next success. Monitors auto-pause only after repeated consecutive failures or insufficient-credit skips; resume by updating status to active.

Pricing

Creating, listing, updating, and reading monitors, runs, and changes is free. You pay per run, priced like the equivalent public endpoint:
Run typeCredits per run
Page check (page + exact)1
Sitemap check (sitemap + exact)1
Semantic extract check (extract + semantic)10
Skipped or failed runs0
Monitor spend appears on the usage page under the path /v1/monitors/run, and GET /monitors/credit-usage returns a per-monitor credit and run breakdown over any time window:
cURL
curl "https://api.context.dev/v1/monitors/credit-usage?since=2026-07-01T00:00:00Z" \
  -H "Authorization: Bearer $CONTEXT_DEV_API_KEY"

Use cases

  • Competitor price tracking: a page monitor on each competitor’s pricing page, with the webhook posting diffs into Slack.
  • Content ops: a sitemap monitor on a competitor’s blog to catch new posts and landing pages the day they ship.
  • Positioning intelligence: an extract + semantic monitor with instructions like “track packaging, pricing, and headline feature claims”; cosmetic rewording won’t fire it.
  • Compliance watch: an extract monitor over terms or privacy pages, alerting only when obligations meaningfully change.
  • Your own site: a sitemap monitor as a tripwire for pages accidentally dropping out of your sitemap after a deploy.

Next steps

Monitors API Reference

Every endpoint, parameter, and response schema.

Extract Structured Data

The schema-driven extraction that powers extract monitors.

Best Practices

Caching, error handling, and key hygiene.

Troubleshooting

Status codes, retry patterns, and common errors.