Changehook API Docs

Monitor websites for changes via API. No dashboard required.

API v1
Auth: Send your API key on every request.
Authorization: Bearer ch_live_xxxxxxxxxxxxxxxxx

Base URL

All endpoints are under:

https://api.changehook.io/v1

Authentication

All API requests require your API key:

Authorization: Bearer ch_live_YOUR_KEY

Your API key is sent by email after your first successful payment. Keep it secret.


Account / Credits

Check your plan status and remaining credits.

GET /v1/account
cURL
curl -s https://api.changehook.io/v1/account \
  -H "Authorization: Bearer ch_live_YOUR_KEY"
Response 200
{
  "email": "name@company.com",
  "plan": "pro",
  "plan_active": true,
  "credits": {
    "monthly_remaining": 8421,
    "prepaid_remaining": 20000,
    "total_remaining": 28421
  }
}

Credits

Credits are used per successful check. No credits are charged if a check fails (timeout, fetch error).

Costs
  • Standard check: 1 credit
  • js_rendering=true: +2 credits (total 3)
  • screenshot=true: +2 credits (requires JS → total 5)
  • js_rendering=true + screenshot=true: total 5

Monthly credits reset each billing period. Top-up credits never expire. API access requires an active subscription.


Monitors

Create a monitor

POST /v1/monitors
Body
{
  "url": "https://shop.example.com/product/123",
  "selector": ".price",
  "interval_seconds": 3600,
  "extract": "text",
  "attr_name": null,
  "normalize": true,
  "ignore_case": false,
  "js_rendering": false,
  "screenshot": false,
  "webhook_url": "https://client.example.com/changehook",
  "webhook_secret": "optional-secret",
  "active": true
}
Fields
  • url (string, required) – target URL
  • selector (string, required) – CSS selector (first match is used)
  • interval_seconds (int, required) – check interval (plan minimum applies)
  • extract (string, optional) – text (default) | html | attr
  • attr_name (string, required if extract=attr) – attribute name, e.g. href
  • normalize (bool, default true) – normalize whitespace (trim + collapse)
  • ignore_case (bool, default false) – case-insensitive compare
  • js_rendering (bool, default false) – render with headless browser before extraction
  • screenshot (bool, default false) – capture screenshot on change (Agency, requires JS)
  • webhook_url (string, optional) – where to POST change events
  • webhook_secret (string, optional) – used to sign webhook payload
  • active (bool, default true)
Note: If screenshot=true, JS rendering is used automatically. Screenshot artifacts are stored only when a change occurs.
cURL
curl -s https://api.changehook.io/v1/monitors \
  -H "Authorization: Bearer ch_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url":"https://shop.example.com/product/123",
    "selector":".price",
    "interval_seconds":3600,
    "extract":"text",
    "js_rendering":false,
    "screenshot":false,
    "webhook_url":"https://client.example.com/changehook",
    "active":true
  }'
Response 201
{
  "id": "b7c6e5c2-aaaa-bbbb-cccc-111122223333",
  "status": "active",
  "next_run_at": "2025-12-27T12:00:00Z"
}

List monitors

GET /v1/monitors?limit=50&cursor=...
cURL
curl -s "https://api.changehook.io/v1/monitors?limit=50" \
  -H "Authorization: Bearer ch_live_YOUR_KEY"
Response 200
{
  "items": [
    {
      "id": "b7c6e5c2-aaaa-bbbb-cccc-111122223333",
      "url": "https://shop.example.com/product/123",
      "selector": ".price",
      "interval_seconds": 3600,
      "active": true,
      "features": {
        "js_rendering": false,
        "screenshot": false
      },
      "credits_per_check": 1,
      "last_checked_at": "2025-12-27T10:00:00Z",
      "last_changed_at": "2025-12-27T09:20:00Z"
    }
  ],
  "next_cursor": null
}

Get monitor

GET /v1/monitors/{id}
Response 200
{
  "id": "b7c6e5c2-aaaa-bbbb-cccc-111122223333",
  "url": "https://shop.example.com/product/123",
  "selector": ".price",
  "interval_seconds": 3600,
  "extract": "text",
  "attr_name": null,
  "normalize": true,
  "ignore_case": false,
  "js_rendering": false,
  "screenshot": false,
  "webhook_url": "https://client.example.com/changehook",
  "active": true,
  "last_value": "€19.99",
  "last_checked_at": "2025-12-27T10:00:00Z",
  "last_changed_at": "2025-12-27T09:20:00Z",
  "fail_count": 0,
  "last_error": null
}

Update monitor

PATCH /v1/monitors/{id}
Body (all optional)
{
  "selector": ".price",
  "interval_seconds": 1800,
  "js_rendering": true,
  "screenshot": false,
  "active": true
}
Response 200
{ "ok": true }

Delete monitor

DELETE /v1/monitors/{id}
Response 200
{ "ok": true }

Changes

Retrieve only actual changes (no noise).

GET /v1/monitors/{id}/changes?limit=50&cursor=...&since=2025-12-01T00:00:00Z
cURL
curl -s "https://api.changehook.io/v1/monitors/MONITOR_ID/changes?limit=50" \
  -H "Authorization: Bearer ch_live_YOUR_KEY"
Response 200
{
  "items": [
    {
      "id": 123,
      "changed_at": "2025-12-27T09:20:00Z",
      "old_value": "€19.99",
      "new_value": "€17.99",
      "artifact": {
        "screenshot_url": null
      }
    }
  ],
  "next_cursor": null
}

Run now

Queue an immediate check for a monitor. This will use credits on success.

POST /v1/monitors/{id}/run
cURL
curl -s -X POST "https://api.changehook.io/v1/monitors/MONITOR_ID/run" \
  -H "Authorization: Bearer ch_live_YOUR_KEY"
Response 200
{ "ok": true }

Webhooks

If webhook_url is set and a change occurs, Changehook sends a POST request to your endpoint. Webhooks are sent only for actual changes (no baseline runs, no errors).

When webhooks are sent
  • ✅ Fetch succeeded
  • ✅ Selector was found
  • ✅ Extraction succeeded
  • ✅ Value changed vs. previous value
When webhooks are NOT sent
  • — Baseline (first successful run)
  • — No change detected
  • — Errors (timeouts, selector_not_found, etc.)
Webhook payload

The request body is JSON (Content-Type: application/json).

{
  "type": "change",
  "monitor_id": "b7c6e5c2-aaaa-bbbb-cccc-111122223333",
  "url": "https://shop.example.com/product/123",
  "changed_at": "2025-12-29T21:06:28.177Z",
  "old_value": "€19.99",
  "new_value": "€17.99",
  "screenshot_url": null
}
Signature (optional, recommended)

If you set webhook_secret, Changehook signs the request body and includes:

X-Changehook-Signature: <hex hmac sha256>
Important: The signature is calculated over the raw request body (exact bytes), not a parsed / re-encoded JSON string. Use the raw body exactly as received.
How the signature is calculated
  • Algorithm: HMAC-SHA256
  • Payload: raw request body
  • Encoding: hex
  • No prefix (it’s just hex, not sha256=...)
Verify signature (PHP)
<?php
$secret = 'YOUR_WEBHOOK_SECRET';

// RAW body (do not json_decode before verifying!)
$raw = file_get_contents('php://input');

$expected = hash_hmac('sha256', $raw, $secret);
$got = $_SERVER['HTTP_X_CHANGEHOOK_SIGNATURE'] ?? '';

if (!hash_equals($expected, $got)) {
  http_response_code(401);
  exit('invalid signature');
}

http_response_code(200);
echo 'ok';
?>
Verify signature (Node.js / Express)
const crypto = require("crypto");

// IMPORTANT: capture rawBody in middleware!
// e.g. express.raw({ type: "application/json" })

const secret = process.env.WEBHOOK_SECRET;
const rawBody = req.body; // Buffer (raw)

const expected = crypto
  .createHmac("sha256", secret)
  .update(rawBody)
  .digest("hex");

const got = req.get("x-changehook-signature") || "";

if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got))) {
  return res.status(401).send("invalid signature");
}

res.status(200).send("ok");
Delivery / retries

Webhooks are sent once per change event. No automatic retries. If your endpoint is temporarily unavailable, the change is still stored for 24 hours and can be fetched via GET /v1/monitors/{id}/changes.


Errors

All errors use the same JSON format:

{
  "error": "invalid_params",
  "message": "selector is required"
}
Common error codes
  • 400 invalid_params
  • 401 unauthorized
  • 402 insufficient_credits
  • 403 plan_inactive
  • 404 not_found
  • 409 limit_reached
  • 429 rate_limited
  • 500 server_error

Quickstart

1) Create a monitor

curl -s https://api.changehook.io/v1/monitors \
  -H "Authorization: Bearer ch_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url":"https://example.com",
    "selector":"h1",
    "interval_seconds":3600
  }'

2) Run now

curl -s -X POST "https://api.changehook.io/v1/monitors/MONITOR_ID/run" \
  -H "Authorization: Bearer ch_live_YOUR_KEY"

3) Fetch changes

curl -s "https://api.changehook.io/v1/monitors/MONITOR_ID/changes" \
  -H "Authorization: Bearer ch_live_YOUR_KEY"