curl --request POST \
--url https://api.prefetch.io/monitor \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"name": "Competitor pricing",
"schedule": {
"cron": "*/30 * * * *",
"text": "every weekday at 9am",
"timezone": "Europe/Athens"
},
"targets": [
{
"type": "scrape",
"urls": [
"https://example.com/pricing"
],
"id": "tgt_8d1e0f2a3b4c5d6e",
"scrape_options": {
"only_main_content": true,
"include_links": true,
"include_images": false,
"include_selectors": [
"#pricing"
],
"exclude_selectors": [
".last-updated",
".testimonial"
]
},
"change_tracking": {
"modes": [
"markdown"
],
"json_prompt": "Each plan: name, monthly price, headline features",
"json_schema": {}
}
}
],
"goal": "Alert me when a plan price or its included features change",
"judge_enabled": true,
"retention_days": 30
}
'import requests
url = "https://api.prefetch.io/monitor"
payload = {
"name": "Competitor pricing",
"schedule": {
"cron": "*/30 * * * *",
"text": "every weekday at 9am",
"timezone": "Europe/Athens"
},
"targets": [
{
"type": "scrape",
"urls": ["https://example.com/pricing"],
"id": "tgt_8d1e0f2a3b4c5d6e",
"scrape_options": {
"only_main_content": True,
"include_links": True,
"include_images": False,
"include_selectors": ["#pricing"],
"exclude_selectors": [".last-updated", ".testimonial"]
},
"change_tracking": {
"modes": ["markdown"],
"json_prompt": "Each plan: name, monthly price, headline features",
"json_schema": {}
}
}
],
"goal": "Alert me when a plan price or its included features change",
"judge_enabled": True,
"retention_days": 30
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'Competitor pricing',
schedule: {cron: '*/30 * * * *', text: 'every weekday at 9am', timezone: 'Europe/Athens'},
targets: [
{
type: 'scrape',
urls: ['https://example.com/pricing'],
id: 'tgt_8d1e0f2a3b4c5d6e',
scrape_options: {
only_main_content: true,
include_links: true,
include_images: false,
include_selectors: ['#pricing'],
exclude_selectors: ['.last-updated', '.testimonial']
},
change_tracking: {
modes: ['markdown'],
json_prompt: 'Each plan: name, monthly price, headline features',
json_schema: {}
}
}
],
goal: 'Alert me when a plan price or its included features change',
judge_enabled: true,
retention_days: 30
})
};
fetch('https://api.prefetch.io/monitor', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.prefetch.io/monitor",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Competitor pricing',
'schedule' => [
'cron' => '*/30 * * * *',
'text' => 'every weekday at 9am',
'timezone' => 'Europe/Athens'
],
'targets' => [
[
'type' => 'scrape',
'urls' => [
'https://example.com/pricing'
],
'id' => 'tgt_8d1e0f2a3b4c5d6e',
'scrape_options' => [
'only_main_content' => true,
'include_links' => true,
'include_images' => false,
'include_selectors' => [
'#pricing'
],
'exclude_selectors' => [
'.last-updated',
'.testimonial'
]
],
'change_tracking' => [
'modes' => [
'markdown'
],
'json_prompt' => 'Each plan: name, monthly price, headline features',
'json_schema' => [
]
]
]
],
'goal' => 'Alert me when a plan price or its included features change',
'judge_enabled' => true,
'retention_days' => 30
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.prefetch.io/monitor"
payload := strings.NewReader("{\n \"name\": \"Competitor pricing\",\n \"schedule\": {\n \"cron\": \"*/30 * * * *\",\n \"text\": \"every weekday at 9am\",\n \"timezone\": \"Europe/Athens\"\n },\n \"targets\": [\n {\n \"type\": \"scrape\",\n \"urls\": [\n \"https://example.com/pricing\"\n ],\n \"id\": \"tgt_8d1e0f2a3b4c5d6e\",\n \"scrape_options\": {\n \"only_main_content\": true,\n \"include_links\": true,\n \"include_images\": false,\n \"include_selectors\": [\n \"#pricing\"\n ],\n \"exclude_selectors\": [\n \".last-updated\",\n \".testimonial\"\n ]\n },\n \"change_tracking\": {\n \"modes\": [\n \"markdown\"\n ],\n \"json_prompt\": \"Each plan: name, monthly price, headline features\",\n \"json_schema\": {}\n }\n }\n ],\n \"goal\": \"Alert me when a plan price or its included features change\",\n \"judge_enabled\": true,\n \"retention_days\": 30\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.prefetch.io/monitor")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"Competitor pricing\",\n \"schedule\": {\n \"cron\": \"*/30 * * * *\",\n \"text\": \"every weekday at 9am\",\n \"timezone\": \"Europe/Athens\"\n },\n \"targets\": [\n {\n \"type\": \"scrape\",\n \"urls\": [\n \"https://example.com/pricing\"\n ],\n \"id\": \"tgt_8d1e0f2a3b4c5d6e\",\n \"scrape_options\": {\n \"only_main_content\": true,\n \"include_links\": true,\n \"include_images\": false,\n \"include_selectors\": [\n \"#pricing\"\n ],\n \"exclude_selectors\": [\n \".last-updated\",\n \".testimonial\"\n ]\n },\n \"change_tracking\": {\n \"modes\": [\n \"markdown\"\n ],\n \"json_prompt\": \"Each plan: name, monthly price, headline features\",\n \"json_schema\": {}\n }\n }\n ],\n \"goal\": \"Alert me when a plan price or its included features change\",\n \"judge_enabled\": true,\n \"retention_days\": 30\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.prefetch.io/monitor")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"Competitor pricing\",\n \"schedule\": {\n \"cron\": \"*/30 * * * *\",\n \"text\": \"every weekday at 9am\",\n \"timezone\": \"Europe/Athens\"\n },\n \"targets\": [\n {\n \"type\": \"scrape\",\n \"urls\": [\n \"https://example.com/pricing\"\n ],\n \"id\": \"tgt_8d1e0f2a3b4c5d6e\",\n \"scrape_options\": {\n \"only_main_content\": true,\n \"include_links\": true,\n \"include_images\": false,\n \"include_selectors\": [\n \"#pricing\"\n ],\n \"exclude_selectors\": [\n \".last-updated\",\n \".testimonial\"\n ]\n },\n \"change_tracking\": {\n \"modes\": [\n \"markdown\"\n ],\n \"json_prompt\": \"Each plan: name, monthly price, headline features\",\n \"json_schema\": {}\n }\n }\n ],\n \"goal\": \"Alert me when a plan price or its included features change\",\n \"judge_enabled\": true,\n \"retention_days\": 30\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"id": "mon_3f9a1c2b7d4e4f6a8b0c1d2e3f4a5b6c",
"name": "Competitor pricing",
"status": "active",
"schedule": {
"cron": "0 9 * * 1-5",
"timezone": "Europe/Athens",
"text": "every weekday at 9am"
},
"next_run_at": "2026-10-12T06:00:00.000Z",
"last_run_at": null,
"current_check_id": null,
"goal": "Alert me when a plan price or its included features change",
"judge_enabled": true,
"targets": [
{
"id": "tgt_8d1e0f2a3b4c5d6e",
"type": "scrape",
"urls": [
"https://example.com/pricing"
],
"scrape_options": {
"only_main_content": true,
"include_links": true,
"include_images": false,
"include_selectors": [],
"exclude_selectors": []
},
"change_tracking": {
"modes": [
"json"
],
"json_prompt": "Each plan: name, monthly price, headline features"
}
},
{
"id": "tgt_1a2b3c4d5e6f7a8b",
"type": "crawl",
"url": "https://example.com/changelog",
"crawl_options": {
"limit": 25,
"max_depth": 2,
"include_paths": [
"^/changelog/"
],
"exclude_paths": [],
"allow_subdomains": false,
"ignore_query_params": true,
"respect_robots_txt": true,
"use_sitemap": true,
"delay_ms": 0,
"concurrency": 2
},
"scrape_options": {
"only_main_content": true,
"include_links": true,
"include_images": false,
"include_selectors": [],
"exclude_selectors": []
},
"change_tracking": {
"modes": [
"markdown"
]
}
}
],
"webhook": {
"url": "https://you.example.org/hooks/prefetch",
"headers": {},
"metadata": {
"env": "production"
},
"events": [
"monitor.page",
"monitor.check.completed"
],
"secret": "whsec_4f1c9e2a7b3d8e6f0a5c1b9d2e7f3a8c4b6d0e1f9a2c5b7d"
},
"retention_days": 30,
"estimated_credits_per_check": 55,
"estimated_credits_per_month": 1155,
"last_check_summary": null,
"created_at": "2026-10-09T10:00:00.000Z",
"updated_at": "2026-10-09T10:00:00.000Z"
},
"meta": {
"requestId": "b8f1d2e3-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"durationMs": 61
}
}{
"error": "Credit limit exceeded",
"error_code": "CREDITS_EXHAUSTED",
"retryable": false,
"success": false,
"retryAfterSeconds": 123,
"limit": 123,
"meta": {
"requestId": "a3f2c1d4-7b6e-4f2a-9c1d-8e3f2a1b4c5d",
"durationMs": 1842
}
}{
"error": "Credit limit exceeded",
"error_code": "CREDITS_EXHAUSTED",
"retryable": false,
"success": false,
"retryAfterSeconds": 123,
"limit": 123,
"meta": {
"requestId": "a3f2c1d4-7b6e-4f2a-9c1d-8e3f2a1b4c5d",
"durationMs": 1842
}
}{
"error": "Credit limit exceeded",
"error_code": "CREDITS_EXHAUSTED",
"retryable": false,
"success": false,
"retryAfterSeconds": 123,
"limit": 123,
"meta": {
"requestId": "a3f2c1d4-7b6e-4f2a-9c1d-8e3f2a1b4c5d",
"durationMs": 1842
}
}{
"error": "Credit limit exceeded",
"error_code": "CREDITS_EXHAUSTED",
"retryable": false,
"success": false,
"retryAfterSeconds": 123,
"limit": 123,
"meta": {
"requestId": "a3f2c1d4-7b6e-4f2a-9c1d-8e3f2a1b4c5d",
"durationMs": 1842
}
}{
"error": "Credit limit exceeded",
"error_code": "CREDITS_EXHAUSTED",
"retryable": false,
"success": false,
"retryAfterSeconds": 123,
"limit": 123,
"meta": {
"requestId": "a3f2c1d4-7b6e-4f2a-9c1d-8e3f2a1b4c5d",
"durationMs": 1842
}
}POST /monitor
Watch pages on a schedule and get told what changed. Free to create; each check costs 1 credit per page it fetches, plus 1 per change judged against your goal.
curl --request POST \
--url https://api.prefetch.io/monitor \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"name": "Competitor pricing",
"schedule": {
"cron": "*/30 * * * *",
"text": "every weekday at 9am",
"timezone": "Europe/Athens"
},
"targets": [
{
"type": "scrape",
"urls": [
"https://example.com/pricing"
],
"id": "tgt_8d1e0f2a3b4c5d6e",
"scrape_options": {
"only_main_content": true,
"include_links": true,
"include_images": false,
"include_selectors": [
"#pricing"
],
"exclude_selectors": [
".last-updated",
".testimonial"
]
},
"change_tracking": {
"modes": [
"markdown"
],
"json_prompt": "Each plan: name, monthly price, headline features",
"json_schema": {}
}
}
],
"goal": "Alert me when a plan price or its included features change",
"judge_enabled": true,
"retention_days": 30
}
'import requests
url = "https://api.prefetch.io/monitor"
payload = {
"name": "Competitor pricing",
"schedule": {
"cron": "*/30 * * * *",
"text": "every weekday at 9am",
"timezone": "Europe/Athens"
},
"targets": [
{
"type": "scrape",
"urls": ["https://example.com/pricing"],
"id": "tgt_8d1e0f2a3b4c5d6e",
"scrape_options": {
"only_main_content": True,
"include_links": True,
"include_images": False,
"include_selectors": ["#pricing"],
"exclude_selectors": [".last-updated", ".testimonial"]
},
"change_tracking": {
"modes": ["markdown"],
"json_prompt": "Each plan: name, monthly price, headline features",
"json_schema": {}
}
}
],
"goal": "Alert me when a plan price or its included features change",
"judge_enabled": True,
"retention_days": 30
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'Competitor pricing',
schedule: {cron: '*/30 * * * *', text: 'every weekday at 9am', timezone: 'Europe/Athens'},
targets: [
{
type: 'scrape',
urls: ['https://example.com/pricing'],
id: 'tgt_8d1e0f2a3b4c5d6e',
scrape_options: {
only_main_content: true,
include_links: true,
include_images: false,
include_selectors: ['#pricing'],
exclude_selectors: ['.last-updated', '.testimonial']
},
change_tracking: {
modes: ['markdown'],
json_prompt: 'Each plan: name, monthly price, headline features',
json_schema: {}
}
}
],
goal: 'Alert me when a plan price or its included features change',
judge_enabled: true,
retention_days: 30
})
};
fetch('https://api.prefetch.io/monitor', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.prefetch.io/monitor",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Competitor pricing',
'schedule' => [
'cron' => '*/30 * * * *',
'text' => 'every weekday at 9am',
'timezone' => 'Europe/Athens'
],
'targets' => [
[
'type' => 'scrape',
'urls' => [
'https://example.com/pricing'
],
'id' => 'tgt_8d1e0f2a3b4c5d6e',
'scrape_options' => [
'only_main_content' => true,
'include_links' => true,
'include_images' => false,
'include_selectors' => [
'#pricing'
],
'exclude_selectors' => [
'.last-updated',
'.testimonial'
]
],
'change_tracking' => [
'modes' => [
'markdown'
],
'json_prompt' => 'Each plan: name, monthly price, headline features',
'json_schema' => [
]
]
]
],
'goal' => 'Alert me when a plan price or its included features change',
'judge_enabled' => true,
'retention_days' => 30
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.prefetch.io/monitor"
payload := strings.NewReader("{\n \"name\": \"Competitor pricing\",\n \"schedule\": {\n \"cron\": \"*/30 * * * *\",\n \"text\": \"every weekday at 9am\",\n \"timezone\": \"Europe/Athens\"\n },\n \"targets\": [\n {\n \"type\": \"scrape\",\n \"urls\": [\n \"https://example.com/pricing\"\n ],\n \"id\": \"tgt_8d1e0f2a3b4c5d6e\",\n \"scrape_options\": {\n \"only_main_content\": true,\n \"include_links\": true,\n \"include_images\": false,\n \"include_selectors\": [\n \"#pricing\"\n ],\n \"exclude_selectors\": [\n \".last-updated\",\n \".testimonial\"\n ]\n },\n \"change_tracking\": {\n \"modes\": [\n \"markdown\"\n ],\n \"json_prompt\": \"Each plan: name, monthly price, headline features\",\n \"json_schema\": {}\n }\n }\n ],\n \"goal\": \"Alert me when a plan price or its included features change\",\n \"judge_enabled\": true,\n \"retention_days\": 30\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.prefetch.io/monitor")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"Competitor pricing\",\n \"schedule\": {\n \"cron\": \"*/30 * * * *\",\n \"text\": \"every weekday at 9am\",\n \"timezone\": \"Europe/Athens\"\n },\n \"targets\": [\n {\n \"type\": \"scrape\",\n \"urls\": [\n \"https://example.com/pricing\"\n ],\n \"id\": \"tgt_8d1e0f2a3b4c5d6e\",\n \"scrape_options\": {\n \"only_main_content\": true,\n \"include_links\": true,\n \"include_images\": false,\n \"include_selectors\": [\n \"#pricing\"\n ],\n \"exclude_selectors\": [\n \".last-updated\",\n \".testimonial\"\n ]\n },\n \"change_tracking\": {\n \"modes\": [\n \"markdown\"\n ],\n \"json_prompt\": \"Each plan: name, monthly price, headline features\",\n \"json_schema\": {}\n }\n }\n ],\n \"goal\": \"Alert me when a plan price or its included features change\",\n \"judge_enabled\": true,\n \"retention_days\": 30\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.prefetch.io/monitor")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"Competitor pricing\",\n \"schedule\": {\n \"cron\": \"*/30 * * * *\",\n \"text\": \"every weekday at 9am\",\n \"timezone\": \"Europe/Athens\"\n },\n \"targets\": [\n {\n \"type\": \"scrape\",\n \"urls\": [\n \"https://example.com/pricing\"\n ],\n \"id\": \"tgt_8d1e0f2a3b4c5d6e\",\n \"scrape_options\": {\n \"only_main_content\": true,\n \"include_links\": true,\n \"include_images\": false,\n \"include_selectors\": [\n \"#pricing\"\n ],\n \"exclude_selectors\": [\n \".last-updated\",\n \".testimonial\"\n ]\n },\n \"change_tracking\": {\n \"modes\": [\n \"markdown\"\n ],\n \"json_prompt\": \"Each plan: name, monthly price, headline features\",\n \"json_schema\": {}\n }\n }\n ],\n \"goal\": \"Alert me when a plan price or its included features change\",\n \"judge_enabled\": true,\n \"retention_days\": 30\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"id": "mon_3f9a1c2b7d4e4f6a8b0c1d2e3f4a5b6c",
"name": "Competitor pricing",
"status": "active",
"schedule": {
"cron": "0 9 * * 1-5",
"timezone": "Europe/Athens",
"text": "every weekday at 9am"
},
"next_run_at": "2026-10-12T06:00:00.000Z",
"last_run_at": null,
"current_check_id": null,
"goal": "Alert me when a plan price or its included features change",
"judge_enabled": true,
"targets": [
{
"id": "tgt_8d1e0f2a3b4c5d6e",
"type": "scrape",
"urls": [
"https://example.com/pricing"
],
"scrape_options": {
"only_main_content": true,
"include_links": true,
"include_images": false,
"include_selectors": [],
"exclude_selectors": []
},
"change_tracking": {
"modes": [
"json"
],
"json_prompt": "Each plan: name, monthly price, headline features"
}
},
{
"id": "tgt_1a2b3c4d5e6f7a8b",
"type": "crawl",
"url": "https://example.com/changelog",
"crawl_options": {
"limit": 25,
"max_depth": 2,
"include_paths": [
"^/changelog/"
],
"exclude_paths": [],
"allow_subdomains": false,
"ignore_query_params": true,
"respect_robots_txt": true,
"use_sitemap": true,
"delay_ms": 0,
"concurrency": 2
},
"scrape_options": {
"only_main_content": true,
"include_links": true,
"include_images": false,
"include_selectors": [],
"exclude_selectors": []
},
"change_tracking": {
"modes": [
"markdown"
]
}
}
],
"webhook": {
"url": "https://you.example.org/hooks/prefetch",
"headers": {},
"metadata": {
"env": "production"
},
"events": [
"monitor.page",
"monitor.check.completed"
],
"secret": "whsec_4f1c9e2a7b3d8e6f0a5c1b9d2e7f3a8c4b6d0e1f9a2c5b7d"
},
"retention_days": 30,
"estimated_credits_per_check": 55,
"estimated_credits_per_month": 1155,
"last_check_summary": null,
"created_at": "2026-10-09T10:00:00.000Z",
"updated_at": "2026-10-09T10:00:00.000Z"
},
"meta": {
"requestId": "b8f1d2e3-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"durationMs": 61
}
}{
"error": "Credit limit exceeded",
"error_code": "CREDITS_EXHAUSTED",
"retryable": false,
"success": false,
"retryAfterSeconds": 123,
"limit": 123,
"meta": {
"requestId": "a3f2c1d4-7b6e-4f2a-9c1d-8e3f2a1b4c5d",
"durationMs": 1842
}
}{
"error": "Credit limit exceeded",
"error_code": "CREDITS_EXHAUSTED",
"retryable": false,
"success": false,
"retryAfterSeconds": 123,
"limit": 123,
"meta": {
"requestId": "a3f2c1d4-7b6e-4f2a-9c1d-8e3f2a1b4c5d",
"durationMs": 1842
}
}{
"error": "Credit limit exceeded",
"error_code": "CREDITS_EXHAUSTED",
"retryable": false,
"success": false,
"retryAfterSeconds": 123,
"limit": 123,
"meta": {
"requestId": "a3f2c1d4-7b6e-4f2a-9c1d-8e3f2a1b4c5d",
"durationMs": 1842
}
}{
"error": "Credit limit exceeded",
"error_code": "CREDITS_EXHAUSTED",
"retryable": false,
"success": false,
"retryAfterSeconds": 123,
"limit": 123,
"meta": {
"requestId": "a3f2c1d4-7b6e-4f2a-9c1d-8e3f2a1b4c5d",
"durationMs": 1842
}
}{
"error": "Credit limit exceeded",
"error_code": "CREDITS_EXHAUSTED",
"retryable": false,
"success": false,
"retryAfterSeconds": 123,
"limit": 123,
"meta": {
"requestId": "a3f2c1d4-7b6e-4f2a-9c1d-8e3f2a1b4c5d",
"durationMs": 1842
}
}Overview
A monitor re-checks pages on a schedule and compares each one with the previous check. Instead of polling a pricing page, a changelog or a policy page yourself, you get a webhook with just the diff when something changes.Create
POST /monitor with what to watch, how often, and — optionally — a goal describing which changes matter.Baseline
Check
same, new, changed, removed or error, with a diff.Get told
GET /monitor/{id}/checks/{checkId}.Example request
curl -X POST "https://api.prefetch.io/monitor" \
-H "X-API-Key: $PREFETCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Competitor pricing",
"schedule": { "text": "every weekday at 9am", "timezone": "Europe/Athens" },
"goal": "Alert me when a plan price or its included features change",
"targets": [
{
"type": "scrape",
"urls": ["https://example.com/pricing"],
"change_tracking": { "modes": ["json"], "json_prompt": "Each plan: name, monthly price, headline features" }
},
{
"type": "crawl",
"url": "https://example.com/changelog",
"crawl_options": { "limit": 25, "include_paths": ["^/changelog/"] }
}
],
"webhook": { "url": "https://you.example.org/hooks/prefetch" }
}'
const res = await fetch("https://api.prefetch.io/monitor", {
method: "POST",
headers: {
"X-API-Key": process.env.PREFETCH_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Competitor pricing",
schedule: { text: "every weekday at 9am", timezone: "Europe/Athens" },
goal: "Alert me when a plan price or its included features change",
targets: [
{
type: "scrape",
urls: ["https://example.com/pricing"],
change_tracking: { modes: ["json"], json_prompt: "Each plan: name, monthly price, headline features" },
},
{
type: "crawl",
url: "https://example.com/changelog",
crawl_options: { limit: 25, include_paths: ["^/changelog/"] },
},
],
webhook: { url: "https://you.example.org/hooks/prefetch" },
}),
});
const { data } = await res.json();
console.log(data.id, data.schedule.cron, data.webhook.secret);
import os, requests
r = requests.post(
"https://api.prefetch.io/monitor",
headers={"X-API-Key": os.environ["PREFETCH_API_KEY"]},
json={
"name": "Competitor pricing",
"schedule": {"text": "every weekday at 9am", "timezone": "Europe/Athens"},
"goal": "Alert me when a plan price or its included features change",
"targets": [
{
"type": "scrape",
"urls": ["https://example.com/pricing"],
"change_tracking": {"modes": ["json"], "json_prompt": "Each plan: name, monthly price, headline features"},
},
{
"type": "crawl",
"url": "https://example.com/changelog",
"crawl_options": {"limit": 25, "include_paths": ["^/changelog/"]},
},
],
"webhook": {"url": "https://you.example.org/hooks/prefetch"},
},
)
monitor = r.json()["data"]
print(monitor["id"], monitor["webhook"]["secret"])
Example response
{
"success": true,
"data": {
"id": "mon_3f9a1c2b7d4e4f6a8b0c1d2e3f4a5b6c",
"name": "Competitor pricing",
"status": "active",
"schedule": { "cron": "0 9 * * 1-5", "timezone": "Europe/Athens", "text": "every weekday at 9am" },
"next_run_at": "2026-10-12T06:00:00.000Z",
"last_run_at": null,
"current_check_id": null,
"goal": "Alert me when a plan price or its included features change",
"judge_enabled": true,
"targets": [
{ "id": "tgt_8d1e0f2a3b4c5d6e", "type": "scrape", "urls": ["https://example.com/pricing"], "...": "every option, with its effective value" },
{ "id": "tgt_1a2b3c4d5e6f7a8b", "type": "crawl", "url": "https://example.com/changelog", "...": "..." }
],
"webhook": {
"url": "https://you.example.org/hooks/prefetch",
"headers": {},
"metadata": {},
"events": ["monitor.page", "monitor.check.completed"],
"secret": "whsec_4f1c9e2a7b3d8e6f0a5c1b9d2e7f3a8c4b6d0e1f9a2c5b7d"
},
"retention_days": 30,
"estimated_credits_per_check": 55,
"estimated_credits_per_month": 1155,
"last_check_summary": null,
"created_at": "2026-10-09T10:00:00.000Z",
"updated_at": "2026-10-09T10:00:00.000Z"
},
"meta": {
"requestId": "b8f1d2e3-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"durationMs": 61
}
}
201 Created. Keep the webhook.secret — you need it to verify each webhook. You can read it again with GET /monitor/{id}.
What to watch
A monitor has up to 50 targets, of two kinds. You can mix them.| Target | Watches | Each check |
|---|---|---|
scrape | A fixed list of up to 100 URLs | Fetches each page the way GET /scrape would, rendering JavaScript and getting past bot protection |
crawl | A site, or a section of one | Re-crawls it with the same options as POST /crawl, so pages that appear or disappear are reported too |
limit.
removed when it runs out of pages to visit. A crawl that stops at its limit cannot tell a page that was taken down from one it never reached, so it reports no removals. Give a crawl target a limit comfortably above the section’s size if removals matter to you.Schedules
Write the schedule as a phrase or as five-field cron, in any IANA timezone:{ "schedule": { "text": "every weekday at 9am", "timezone": "America/New_York" } }
{ "schedule": { "cron": "*/30 * * * *" } }
every 30 minutes, every 15 minutes starting at :07, hourly, every 2 hours, daily, daily at 9am, daily at 5:30 PM, every weekday at 9am, weekly, every monday at 8:00 and monthly.
- Runs must be at least 5 minutes apart — every gap, not just the first.
- Phrases that leave the minute open (
hourly,daily) get a fixed minute of their own, so monitors created at the same time do not all fire at once. - The response always shows the cron the monitor actually runs on.
- Times follow the timezone across daylight-saving changes.
Detecting changes
Each page of each check gets one status:| Status | Meaning |
|---|---|
same | Fetched again; nothing changed. |
new | Seen for the first time — on the first check, or a page that appeared since. |
changed | Fetched again; the content differs. The page carries a diff. |
removed | A page the monitor knew answered 404 or 410, or a crawl that ran to completion no longer finds it. Reported once. |
error | Could not be checked. The last good snapshot is kept, so the next good check compares against it. |
diff.text holds a unified diff with the same hunks structured in diff.hunks. Whitespace and line-ending churn never counts as a change.
To watch specific facts instead of the whole text, use the json mode. Describe the fields, and each check extracts them and compares them field by field:
{
"type": "scrape",
"urls": ["https://example.com/pricing"],
"change_tracking": { "modes": ["json"], "json_prompt": "Each plan: name, monthly price, headline features" }
}
changed only when a tracked field changes, and diff.fields says exactly which one: plans[0].price: "$19/mo" → "$24/mo". Pass json_schema instead of, or as well as, json_prompt to fix the shape. Use "modes": ["json", "markdown"] to get both diffs.
scrape_options.exclude_selectors, or switch to the json mode and track only what you care about. Images are left out of the comparison by default, because their URLs change with every deploy.Goals and judging
Add agoal — one plain sentence about what you want to be told — and every changed, new or removed page is judged against it:
{ "goal": "Alert me when a plan price or its included features change" }
is_meaningful and a judgment with a reason. Pages ruled not meaningful are still recorded on the check, but don’t send a webhook. Good goals restate their scope (“the Pro plan”, “the top 10”, “API changes”) and name exclusions only when they matter.
Judging is on whenever a goal is set; pass judge_enabled: false to keep a goal without judging. The first check’s baseline is never judged.
Webhooks
With awebhook, two events are POSTed to you:
| Event | When |
|---|---|
monitor.page | A page changed, appeared, was removed, or started failing — and, with a goal, the judge ruled it meaningful |
monitor.check.completed | A check finished, with its summary counts |
webhook.secret. Answer 2xx within 10 seconds; failures are retried after 1, 5 and 15 minutes. The webhook URL must use https://.
{
"webhook": {
"url": "https://you.example.org/hooks/prefetch",
"headers": { "Authorization": "Bearer your-token" },
"metadata": { "env": "production" },
"events": ["monitor.page"]
}
}
Credits
Creating, reading and changing a monitor is free. Each check is billed to the API key that created the monitor:- 1 credit per page fetched — the
/scraperate, or 4 on targets trackingjson. - 1 credit per page judged against your goal — changed, new or removed pages only.
estimated_credits_per_month is an upper bound: it assumes every page is fetched and judged on every check.
A check never spends more than the key has left. When credits run out partway, it stops and reports partial; when the key has none at all, the check is recorded as skipped_no_credits and the monitor keeps its schedule, resuming as soon as credits refill.
limit: 100 on a 5-minute schedule fetches 28,800 pages a day. Start small, and pause monitors you are not using.Limits
| Limit | Value |
|---|---|
| Targets per monitor | 50 |
| URLs per scrape target | 100 |
| Pages per check | 500 |
| Monitors per API key | 50 |
| Shortest schedule | Every 5 minutes |
| Check history | retention_days, 30 by default, up to 365 |
Next
GET /monitor/{id}/checks/{checkId}
monitor.page webhook
POST /monitor/{id}/run
PATCH /monitor/{id}
Authorizations
Your Prefetch API key. Obtain one from the dashboard.
Body
256"Competitor pricing"
Exactly one of cron or text. A bare string is accepted too, and read as cron when it has five cron fields.
Show child attributes
Show child attributes
What to watch. At most 500 pages per check across all targets.
1 - 50 elementsWatches a fixed list of pages, each fetched the way GET /scrape would fetch it.
- Scrape target
- Crawl target
Show child attributes
Show child attributes
What you want to be told about, in one plain sentence. With a goal, every changed page is judged against it, and only meaningful changes send a monitor.page webhook. 1 credit per page judged.
2000"Alert me when a plan price or its included features change"
Judge changes against the goal. Default: true when a goal is set.
Show child attributes
Show child attributes
How long check history is kept.
1 <= x <= 365