Examples
Wire a page-change into whatever you already run
Everything the dashboard does is available over REST with an X-API-Key header. Full OpenAPI at /docs. Get a key by creating a free account; it is shown once on the dashboard.
1. Create a monitor with curl
Watch a competitor's pricing region every 15 minutes, ignore “updated N minutes ago” noise, email yourself and POST to a webhook.
curl -sS -X POST https://signalwatch.litework.me/api/v1/checks \
-H "X-API-Key: $SIGNALWATCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Visualping pricing",
"url": "https://visualping.io/pricing",
"css_selector": "#pricing",
"ignore_regex": "\\d+ (minutes|hours) ago",
"min_change_ratio": 0.005,
"ignore_order": true,
"interval_seconds": 900,
"notify_email": true,
"webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX"
}'
Then fire a synthetic event so you can see the payload land before a real change happens:
curl -sS -X POST https://signalwatch.litework.me/api/v1/checks/123/test \ -H "X-API-Key: $SIGNALWATCH_KEY"
History with diffs for a monitor:
curl -sS https://signalwatch.litework.me/api/v1/checks/123/events?limit=20 \ -H "X-API-Key: $SIGNALWATCH_KEY"
2. The webhook payload
Generic HTTPS endpoints receive the raw event. Slack and Discord webhook URLs receive a formatted message with a fenced diff block instead.
POST /your/endpoint
Content-Type: application/json
User-Agent: SignalWatch/1.0
X-SignalWatch-Event: content.changed
X-SignalWatch-Delivery: 5f0c…
X-SignalWatch-Timestamp: 1758230400
X-SignalWatch-Signature: sha256=9b3e…
{
"check_id": 123,
"name": "Visualping pricing",
"url": "https://visualping.io/pricing",
"status_code": 200,
"detected_at": "2026-09-18T21:00:00+00:00",
"event": "content.changed",
"content_hash": "def2…",
"excerpt": "Starter $14/mo …",
"change_ratio": 0.031,
"diff": "--- before\n+++ after\n@@ -3 +3 @@\n-Starter $10/mo\n+Starter $14/mo"
}
Events: content.changed, check.down (with error), check.up, cert.expiring, and test. Real deliveries retry up to 8 times over about 4 hours on network errors, 429, and 5xx. A test alert tries 3 times immediately.
3. Verify the signature
Your webhook signing secret is on the dashboard (“Webhook signing secret”). The signature is HMAC-SHA256(secret, timestamp + "." + raw_body). Reject anything older than a few minutes to stop replays.
Python (Flask)
import hmac, hashlib, time, os
from flask import Flask, request, abort
SECRET = os.environ["SIGNALWATCH_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
@app.post("/signalwatch")
def hook():
ts = request.headers.get("X-SignalWatch-Timestamp", "")
sig = request.headers.get("X-SignalWatch-Signature", "")
if abs(time.time() - int(ts or 0)) > 300:
abort(400)
expected = "sha256=" + hmac.new(SECRET, f"{ts}.".encode() + request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
event = request.get_json()
if event["event"] == "content.changed":
print(event["name"], "changed:\n", event["diff"])
return "", 204
Node (Express)
import express from "express";
import crypto from "node:crypto";
const secret = process.env.SIGNALWATCH_WEBHOOK_SECRET;
const app = express();
app.post("/signalwatch", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.get("X-SignalWatch-Timestamp") ?? "";
const sig = req.get("X-SignalWatch-Signature") ?? "";
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(`${ts}.`).update(req.body).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
console.log(event.event, event.name, event.diff);
res.sendStatus(204);
});
4. n8n / Make / Zapier
- Add a Webhook trigger node (POST). Copy its production URL.
- Paste that URL as the monitor's
webhook_urlin SignalWatch. Press Send test alert; the node receives thetestevent and you can map fields. - Branch on
{{$json.body.event}}. Forcontent.changed,{{$json.body.diff}}holds the unified diff — post it to Slack, open a ticket, write a row to a sheet, or feed it to an LLM node for a one-line summary. - Optional: add a Crypto node to verify
X-SignalWatch-Signatureas above.
The same shape works for Make (Custom webhook) and Zapier (Webhooks by Zapier → Catch Hook).
5. Slack and Discord in 30 seconds
Slack: create an Incoming Webhook for a channel (Slack → Apps → Incoming Webhooks), paste the https://hooks.slack.com/services/… URL into the monitor. Discord: channel settings → Integrations → Webhooks → New, paste the URL. Both get a headline, the URL, the changed percentage and a ```diff block. No paid tier, no app to install.
6. GitHub Actions: fail CI when an upstream page changes
Useful for “our docs say X, vendor changed X.” Runs on a schedule and reads the latest event.
name: upstream-drift
on:
schedule: [{ cron: "0 */6 * * *" }]
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Fail if the vendor page changed in the last 6h
env:
KEY: ${{ secrets.SIGNALWATCH_KEY }}
CHECK_ID: 123
run: |
latest=$(curl -sS "https://signalwatch.litework.me/api/v1/checks/$CHECK_ID/events?limit=1" -H "X-API-Key: $KEY")
event=$(echo "$latest" | jq -r '.[0].event // "none"')
when=$(echo "$latest" | jq -r '.[0].created_at // "1970-01-01T00:00:00Z"')
age=$(( $(date +%s) - $(date -d "$when" +%s) ))
if [ "$event" = "changed" ] && [ "$age" -lt 21600 ]; then
echo "::error::Upstream page changed:"; echo "$latest" | jq -r '.[0].diff'; exit 1
fi
echo "no recent change ($event, ${age}s ago)"
7. Python client in ten lines
import httpx
class SignalWatch:
def __init__(self, key: str, base="https://signalwatch.litework.me"):
self.c = httpx.Client(base_url=base, headers={"X-API-Key": key}, timeout=30)
def create(self, name, url, **opts):
return self.c.post("/api/v1/checks", json={"name": name, "url": url, **opts}).raise_for_status().json()
def events(self, check_id, limit=20):
return self.c.get(f"/api/v1/checks/{check_id}/events", params={"limit": limit}).raise_for_status().json()
def run_now(self, check_id):
return self.c.post(f"/api/v1/checks/{check_id}/run").raise_for_status().json()
sw = SignalWatch("sw_…")
m = sw.create("Visualping pricing", "https://visualping.io/pricing", css_selector="main", notify_email=True)
print(sw.run_now(m["id"])["last_hash"])
Limits worth knowing
- Free: 3 monitors, 15-minute minimum interval. Pro: 50 at 5 minutes. Business: 500 at 2 minutes.
- Public http(s) URLs only. Private ranges, internal hostnames and credentials-in-URL are rejected.
- Fetches are capped at 2 MB and follow up to 3 redirects. JavaScript is not executed; if the price only exists after client-side rendering, we will not see it.
POST /checks/{id}/runand/testare rate-limited to protect the targets you monitor.