Changehook API Docs
Monitor websites for changes via API. No dashboard required.
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 URLselector(string, required) – CSS selector (first match is used)interval_seconds(int, required) – check interval (plan minimum applies)extract(string, optional) –text(default) |html|attrattr_name(string, required if extract=attr) – attribute name, e.g.hrefnormalize(bool, default true) – normalize whitespace (trim + collapse)ignore_case(bool, default false) – case-insensitive comparejs_rendering(bool, default false) – render with headless browser before extractionscreenshot(bool, default false) – capture screenshot on change (Agency, requires JS)webhook_url(string, optional) – where to POST change eventswebhook_secret(string, optional) – used to sign webhook payloadactive(bool, default true)
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).
- ✅ Fetch succeeded
- ✅ Selector was found
- ✅ Extraction succeeded
- ✅ Value changed vs. previous value
- — 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>
How the signature is calculated
- Algorithm:
HMAC-SHA256 - Payload: raw request body
- Encoding: hex
- No prefix (it’s just hex, not
sha256=...)
<?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';
?>
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
400invalid_params401unauthorized402insufficient_credits403plan_inactive404not_found409limit_reached429rate_limited500server_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"