// developer docs

Webhook Alerts

Send change events to any HTTPS endpoint — Zapier, n8n, Make, custom CRMs, internal tools, or your own server. Available on Pro and Business plans.

Overview

When Diffwatch detects a change on a watched URL, it POSTs a JSON payload to every enabled webhook channel for that watch. The payload schema is stable — new fields may be added, but existing fields will not be renamed or removed.

Zapier
Use "Catch Hook" trigger → map fields
n8n
Webhook node → any action node
Make (Integromat)
Webhooks → "Custom webhook" trigger
Custom server
Any HTTPS endpoint that returns 2xx

Payload schema

Every change event delivers this JSON object:

{
  "event": "change_detected",
  "watch": {
    "id": 12345,
    "url": "https://example.com/pricing",
    "selector_type": "css_selector",       // full_page | css_selector | text_contains | regex
    "selector_value": ".price",             // null when selector_type is full_page
    "check_interval_minutes": 5
  },
  "change": {
    "id": 67890,
    "before_hash": "sha256hex...",           // may be null for first detection
    "after_hash": "sha256hex...",
    "ai_summary": "Pricing plan changed from $9 to $12/mo",
    "diff_excerpt": "...first 300 chars of new content...",
    "detected_at": "2026-06-14T12:00:00.000Z"
  },
  "manage_url": "https://diffwatch-ai.polsia.app/manage?token=..."
}
FieldTypeDescription
eventstringAlways "change_detected" for alert events.
watch.idnumberDiffwatch internal watch ID.
watch.urlstringThe monitored URL.
watch.selector_typestringfull_page | css_selector | text_contains | regex
watch.selector_valuestring|nullThe CSS selector, text, or regex pattern. null for full-page watches.
watch.check_interval_minutesnumberHow often the URL is checked.
change.idnumber|nullChange event ID for deduplication.
change.ai_summarystring|nullOne-sentence AI description of what changed.
change.diff_excerptstring|nullFirst 300 characters of the new content.
change.detected_atstring (ISO 8601)Timestamp when the change was detected.
manage_urlstringSigned manage link for the watch owner.

Request headers

Every webhook request includes:

Content-Type: application/json
User-Agent: Diffwatch/1.0
X-Diffwatch-Signature: sha256=<hex>   (only when a secret is configured)

Signature verification

Recommended. Set a secret when adding your webhook. We sign every request body with HMAC-SHA256 and include the signature in X-Diffwatch-Signature. Verify it in your receiver to reject spoofed requests.

Node.js

const crypto = require('crypto');

function verifyDiffwatchSignature(rawBody, secret, signatureHeader) {
  if (!signatureHeader) return false;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  // Constant-time comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(expected)
  );
}

// Express example
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
  const sig = req.headers['x-diffwatch-signature'];
  if (!verifyDiffwatchSignature(req.body, process.env.WEBHOOK_SECRET, sig)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body);
  console.log('Change detected:', event.change.ai_summary);
  res.sendStatus(200);
});

Python

import hmac, hashlib

def verify_diffwatch_signature(raw_body: bytes, secret: str, signature_header: str) -> bool:
    if not signature_header:
        return False
    expected = 'sha256=' + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature_header, expected)

# Flask example
from flask import Flask, request, abort
app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    sig = request.headers.get('X-Diffwatch-Signature', '')
    if not verify_diffwatch_signature(request.get_data(), 'your-secret', sig):
        abort(401)
    event = request.json
    print('Change:', event['change']['ai_summary'])
    return '', 200

Retry behavior

Diffwatch sends one request per change event. If your endpoint returns a 5xx response, we retry once immediately. If the retry also fails, the event is logged as failed and not retried again — ensuring your endpoint isn't flooded. 4xx responses are treated as permanent failures (misconfigured endpoint) and are not retried.

Setting up in Zapier

  1. Create a new Zap → choose Webhooks by Zapier as the trigger → select Catch Hook.
  2. Copy the Zapier webhook URL.
  3. In Diffwatch, go to your watch → Alert channels+ Add webhook.
  4. Paste the URL and click Send test — Zapier will show the test event.
  5. Map change.ai_summary, watch.url, etc. to your Zap action.

Setting up in n8n

  1. Add a Webhook node → set HTTP Method to POST.
  2. Copy the webhook URL (use the "Test URL" while setting up, then switch to "Production URL").
  3. Add your webhook in Diffwatch with that URL and click Send test.
  4. The Webhook node will capture the payload — connect your next node (Slack, email, database, etc.).

Dedup window

Diffwatch enforces a 10-minute dedup window per watch — you'll receive at most one alert every 10 minutes per watch, even if the page changes multiple times within that window.

← Back to pricing | Start watching →