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": "Acme pricing",
"url": "https://acme.example/pricing",
"css_selector": "#pricing",
"ignore_regex": "\\d+ (minutes|hours) ago",
"min_change_ratio": 0.005,
"interval_seconds": 900,
"notify_email": true,
"webhook_url": "https://hooks.example/signalwatch"
}'
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": "Acme pricing",
"url": "https://acme.example/pricing",
"status_code": 200,
"detected_at": "2026-09-18T21:00:00+00:00",
"event": "content.changed",
"content_hash": "def2…",
"excerpt": "Pro $15/mo Business $29/mo …",
"change_ratio": 0.031,
"diff": "--- before\n+++ after\n@@ -3 +3 @@\n-Pro $12/mo\n+Pro $15/mo"
}
Events: content.changed, check.down (with error), check.up, test. Deliveries retry three times on network errors, 429 and 5xx.
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("Stripe pricing", "https://stripe.com/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.