Site SpySite Spy
← Blog

Website Change Detection API: Complete Developer Guide

Why use an API for website change detection

Building your own website change detection system sounds straightforward at first. Fetch a page, store the content, compare it later. But the reality is far more complex.

You need to handle JavaScript-rendered pages. You need to deal with anti-bot protections, CAPTCHAs, and rate limiting from target sites. You need infrastructure that runs reliably on a schedule -- cron jobs that actually fire, a database to store snapshots, a diffing engine that produces useful output instead of noise, and a notification pipeline. Then there is the operational overhead: monitoring your monitors, handling failures gracefully, and scaling when you go from 5 watched pages to 500.

An API-based approach lets you skip all of that. You make HTTP requests to create watches, configure what to track, and receive notifications when changes happen. The infrastructure, rendering, and diffing are handled for you.

Here is what you get with the Site Spy API specifically:

  • Full browser rendering -- JavaScript-heavy pages are rendered before comparison, so you catch changes that simple HTTP fetches miss.
  • Element-level targeting -- Monitor a specific CSS selector instead of the entire page. No more false positives from ads or timestamps.
  • Diff output -- Get structured diffs showing exactly what changed between two snapshots.
  • Flexible notifications -- Webhooks, Slack, Telegram, Discord, email, and dozens more via Apprise.
  • OpenAPI spec -- A complete spec you can use to generate clients in any language.

Getting started

1. Create an account

Sign up at sitespy.app and navigate to your dashboard. The free tier gives you 5 watches and 100 API requests per day -- enough to build and test your integration.

2. Get your API key

In the dashboard, go to Settings and generate an API key. This key authenticates all your API requests.

3. Make your first request

The API base URL is:

https://detect.coolify.vkuprin.com/api/v1

All requests require the x-api-key header. Here is a quick test to list your existing watches:

curl -s -H "x-api-key: YOUR_API_KEY" \
  https://detect.coolify.vkuprin.com/api/v1/watch | python3 -m json.tool

If you get back a JSON object (even an empty one), you are connected and authenticated.

Core API endpoints

Create a watch

import requests

API_BASE = "https://detect.coolify.vkuprin.com/api/v1"
HEADERS = {"x-api-key": "YOUR_API_KEY"}

response = requests.post(
    f"{API_BASE}/watch",
    headers=HEADERS,
    json={
        "url": "https://example.com/pricing",
        "title": "Example Pricing Page",
        "time_between_check": {"minutes": 30},
        "css_filter": ".pricing-table",
        "tags": ["pricing", "competitor"],
    },
)

watch = response.json()
print(f"Watch created: {watch['uuid']}")
const API_BASE = "https://detect.coolify.vkuprin.com/api/v1";
const HEADERS = { "x-api-key": "YOUR_API_KEY" };

const response = await fetch(`${API_BASE}/watch`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({
    url: "https://example.com/pricing",
    title: "Example Pricing Page",
    time_between_check: { minutes: 30 },
    css_filter: ".pricing-table",
    tags: ["pricing", "competitor"],
  }),
});

const watch = await response.json();
console.log("Watch created:", watch.uuid);

Key parameters:

ParameterTypeDescription
urlstringThe URL to monitor (required)
titlestringA human-readable label
time_between_checkobjectCheck interval, e.g. {"minutes": 30} or {"hours": 6}
css_filterstringCSS selector to narrow monitoring to a specific element
tagsarrayTags for organizing watches
notification_urlsarrayApprise notification URLs (see Webhooks section below)

List all watches

response = requests.get(f"{API_BASE}/watch", headers=HEADERS)
watches = response.json()

for uuid, watch in watches.items():
    print(f"{watch['title']} - {watch['url']} - Last changed: {watch.get('last_changed')}")
const response = await fetch(`${API_BASE}/watch`, { headers: HEADERS });
const watches = await response.json();

for (const [uuid, watch] of Object.entries(watches)) {
  console.log(`${watch.title} - ${watch.url} - Last changed: ${watch.last_changed}`);
}

The response is an object keyed by watch UUID, with each value containing the watch configuration and status.

Get watch details

uuid = "your-watch-uuid"
response = requests.get(f"{API_BASE}/watch/{uuid}", headers=HEADERS)
watch = response.json()

print(f"URL: {watch['url']}")
print(f"Last checked: {watch['last_checked']}")
print(f"Last changed: {watch['last_changed']}")
print(f"Check count: {watch['check_count']}")

Update a watch

response = requests.put(
    f"{API_BASE}/watch/{uuid}",
    headers=HEADERS,
    json={
        "time_between_check": {"hours": 1},
        "css_filter": ".price-current",
        "notification_urls": ["json://your-webhook.example.com/endpoint"],
    },
)
await fetch(`${API_BASE}/watch/${uuid}`, {
  method: "PUT",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify({
    time_between_check: { hours: 1 },
    css_filter: ".price-current",
    notification_urls: ["json://your-webhook.example.com/endpoint"],
  }),
});

You only need to include the fields you want to change. Omitted fields retain their current values.

Get change history

Every time a change is detected, a snapshot is stored with a Unix timestamp key. You can list all change timestamps for a watch:

response = requests.get(f"{API_BASE}/watch/{uuid}/history", headers=HEADERS)
history = response.json()

# history is a dict of {unix_timestamp: snapshot_path}
timestamps = sorted(history.keys())
print(f"Total changes detected: {len(timestamps)}")
print(f"Most recent change: {timestamps[-1]}")

Get a diff between two snapshots

This is where it gets powerful. You can request a diff between any two snapshots by passing their Unix timestamps:

timestamps = sorted(history.keys())
if len(timestamps) >= 2:
    older = timestamps[-2]
    newer = timestamps[-1]

    response = requests.get(
        f"{API_BASE}/watch/{uuid}/difference/{older}/{newer}",
        headers=HEADERS,
    )
    diff_text = response.text
    print(diff_text)
const historyRes = await fetch(`${API_BASE}/watch/${uuid}/history`, { headers: HEADERS });
const history = await historyRes.json();
const timestamps = Object.keys(history).sort();

if (timestamps.length >= 2) {
  const older = timestamps[timestamps.length - 2];
  const newer = timestamps[timestamps.length - 1];

  const diffRes = await fetch(
    `${API_BASE}/watch/${uuid}/difference/${older}/${newer}`,
    { headers: HEADERS }
  );
  const diff = await diffRes.text();
  console.log(diff);
}

The response contains a unified diff showing added and removed lines, making it easy to parse programmatically.

Building a price tracker: end-to-end example

Let's build a complete price tracker in Python that monitors a product page and sends you a notification when the price drops.

import requests
import time
import re

API_BASE = "https://detect.coolify.vkuprin.com/api/v1"
HEADERS = {"x-api-key": "YOUR_API_KEY"}

# Step 1: Create a watch targeting the price element
response = requests.post(
    f"{API_BASE}/watch",
    headers=HEADERS,
    json={
        "url": "https://store.example.com/product/wireless-headphones",
        "title": "Wireless Headphones Price",
        "time_between_check": {"minutes": 60},
        "css_filter": ".product-price",
        "notification_urls": [
            "json://your-server.example.com/price-webhook"
        ],
    },
)

watch_uuid = response.json()["uuid"]
print(f"Tracking price with watch: {watch_uuid}")

# Step 2: Poll for changes (or rely on webhooks -- see next section)
def check_for_price_drop():
    history_resp = requests.get(
        f"{API_BASE}/watch/{watch_uuid}/history",
        headers=HEADERS,
    )
    history = history_resp.json()
    timestamps = sorted(history.keys())

    if len(timestamps) < 2:
        print("Not enough snapshots yet. Waiting for changes.")
        return

    older = timestamps[-2]
    newer = timestamps[-1]

    diff_resp = requests.get(
        f"{API_BASE}/watch/{watch_uuid}/difference/{older}/{newer}",
        headers=HEADERS,
    )
    diff_text = diff_resp.text

    # Extract prices from the diff
    removed_prices = re.findall(r"^-.*\$(\d+\.\d{2})", diff_text, re.MULTILINE)
    added_prices = re.findall(r"^\+.*\$(\d+\.\d{2})", diff_text, re.MULTILINE)

    if removed_prices and added_prices:
        old_price = float(removed_prices[0])
        new_price = float(added_prices[0])

        if new_price < old_price:
            drop = old_price - new_price
            print(f"Price dropped! ${old_price} -> ${new_price} (save ${drop:.2f})")
        else:
            print(f"Price changed: ${old_price} -> ${new_price}")

# Step 3: Run periodically
while True:
    check_for_price_drop()
    time.sleep(3600)  # Check every hour

This script creates a watch, then polls the history for diffs. In production, you would replace the polling loop with a webhook handler (covered next), but polling works well for scripts and cron jobs.

Webhooks and notifications

Site Spy uses the Apprise notification format, which supports 100+ services through URL schemes. You set notification URLs on a per-watch basis via the notification_urls field.

JSON webhook

Send a POST request with the change details to your own server:

json://your-server.example.com/webhook-endpoint

The payload includes the watch URL, title, diff content, and metadata. This is the most flexible option for building custom integrations.

Slack

slack://TokenA/TokenB/TokenC/#channel

Generate a Slack incoming webhook and convert it to the Apprise format. Messages will include the watch title, URL, and a summary of what changed.

Telegram

tgram://BOT_TOKEN/CHAT_ID

Create a Telegram bot via @BotFather, get the bot token and your chat ID, and plug them in.

Example: setting notifications on a watch

requests.put(
    f"{API_BASE}/watch/{uuid}",
    headers=HEADERS,
    json={
        "notification_urls": [
            "json://your-server.example.com/webhook",
            "tgram://123456:ABC-DEF/987654321",
            "slack://T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX/#alerts",
        ]
    },
)

You can attach multiple notification URLs to a single watch. All of them fire when a change is detected.

Rate limits

API rate limits depend on your plan:

PlanPriceWatchesAPI requests/day
Free$05100
Starter$4.99/mo251,000
Pro$12.99/mo10010,000
Business$34.99/moUnlimited50,000

Rate limit headers are included in every response:

  • X-RateLimit-Limit -- your daily quota
  • X-RateLimit-Remaining -- requests left today
  • X-RateLimit-Reset -- Unix timestamp when the limit resets

If you exceed your limit, the API returns a 429 Too Many Requests response. The free tier is generous enough for development and testing. Upgrade when you are ready to go to production.

AI assistant integration

Site Spy also provides an MCP (Model Context Protocol) server package -- @site-spy/mcp-server -- that lets AI assistants like Claude interact with your watches directly. If you are building AI-powered workflows that need to monitor websites, check the documentation for setup instructions.

Next steps

You now have everything you need to start monitoring websites programmatically with the Site Spy API.

Here is what to do next:

  1. Get your API key -- Sign up at sitespy.app and generate a key from your dashboard.
  2. Explore the full API reference -- Visit the developer docs for the complete OpenAPI specification, including request/response schemas for every endpoint.
  3. Check pricing -- See the pricing page for plan details and choose the tier that fits your usage.

The full OpenAPI spec is available for download, so you can generate typed clients in any language -- TypeScript, Go, Rust, Java, or whatever your stack requires. Start with the free tier, build your integration, and scale up when you need more capacity.

Start monitoring websites for free

Track up to 5 URLs with hourly checks. No credit card required.