托管 监控
把一组页面和一个 cron 计划交给 CrawlForge。它在自己的调度器上抓取并对比这些页面,记录每一次检查,并通过邮件或已签名的 webhook 告诉您变更了什么。没有按监控收取的费用:每次检查按每个已对比目标收取 track_changes 的价格。
概览
一个监控由名称、最多 20 个目标(一个 url 加可选的 CSS selector)、带 IANA 时区的五段式 cron 计划,以及结果的发送地址组成。CrawlForge 的调度器每 5 分钟运行一次并自行启动每个到期的检查,因此您这边不需要运行任何进程。
对目标的第一次检查会捕获基线并报告为 new。之后的每次检查都与上一次检查对比并把基线向前滚动,因此每次检查报告的是自上一次以来的变更。基线一直保留到监控被删除;检查在 retention_days 之后被清理。
所有端点都接受与工具相同的 X-API-Key 请求头(或 Authorization: Bearer cf_…)。管理调用不计费,且在零 credits 时也能使用。响应使用工具的信封格式 { success, data },错误携带 error.code 和 error.message。
端点
| 方法 | 路径 | 作用 |
|---|---|---|
| POST | /api/v1/monitors | 创建监控。返回 201 及该监控,包括其 webhook_secret。 |
| GET | /api/v1/monitors | 列出监控,每个都带有最新一次检查 last_check。查询参数 limit 和 cursor;响应体携带 next_cursor。绝不返回 webhook_secret。 |
| GET | /api/v1/monitors/{id} | 单个监控,包括 webhook_secret。 |
| PATCH | /api/v1/monitors/{id} | 更新创建字段的任意子集。把 status 设为 paused 会将 next_run_at 置为 null。 |
| DELETE | /api/v1/monitors/{id} | 删除监控。返回 { id, deleted: true }。 |
| POST | /api/v1/monitors/{id}/run | 立即内联运行一次检查,并连同 pages 一起返回。已有检查在进行中时返回 409 MONITOR_RUNNING。 |
| GET | /api/v1/monitors/{id}/checks | 不含 pages 的检查历史。查询参数 limit 和 cursor。 |
| GET | /api/v1/monitors/{id}/checks/{check_id} | 单次检查,含 pages 和 webhook deliveries。 |
创建监控
只有 name 和 targets 是必填项。默认值会为您提供 UTC 时区下每小时一次的检查和 30 天的历史记录。
curl -X POST https://crawlforge.dev/api/v1/monitors \
-H "X-API-Key: cf_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "competitor pricing",
"targets": [{ "url": "https://competitor.com/pricing", "selector": ".pricing-table" }],
"schedule_cron": "0 * * * *",
"webhook_url": "https://example.com/hooks/crawlforge"
}'请求体接受的全部字段。PATCH 接受这些字段的任意子集。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name | string | 必填 | 1 到 80 个字符。 |
targets | array | 必填 | 1 到 20 个 { url, selector? } 对象。URL 必须是公网 http(s) 地址;私有或本地地址会以 400 拒绝。 |
schedule_cron | string | 0 * * * * | 五段式 cron 表达式。相邻两次运行至少间隔 5 分钟。 |
timezone | string | UTC | 计算计划时所用的 IANA 时区,例如 Europe/Madrid。 |
notify_emails | string[] | — | 最多 5 个邮箱地址,当一次检查发现变化时收到邮件。见“通知”。 |
webhook_url | string | — | 接收已签名事件的公网 https URL。见“通知”。 |
webhook_secret | string | 自动生成 | 16 到 128 个字符,用于为每次投递签名。省略且已设置 webhook_url 时自动生成。在创建和 GET 单个时返回,列表中绝不返回。 |
retention_days | number | 30 | 1 到 365。早于该天数的检查会被清理;基线一直保留到监控被删除。 |
status | string | active | active 或 paused。暂停的监控保留其基线,且没有 next_run_at。 |
完整请求体
两个目标、马德里时区的工作日早晨、邮件加使用自有密钥的 webhook、90 天历史记录:
{
"name": "competitor pricing",
"targets": [
{ "url": "https://competitor.com/pricing", "selector": ".pricing-table" },
{ "url": "https://competitor.com/changelog" }
],
"schedule_cron": "0 9 * * 1-5",
"timezone": "Europe/Madrid",
"notify_emails": ["alerts@example.com"],
"webhook_url": "https://example.com/hooks/crawlforge",
"webhook_secret": "3f9a1c7e5b2d48f0a6c1e9d7b5a3f2c4",
"retention_days": 90,
"status": "active"
}监控对象
创建、GET 单个和 PATCH 都返回该监控。next_run_at 是下一个时间槽,last_check_at 是最新一次检查的时间,estimated_credits_per_month 在“计费”中说明。列表响应会额外附上 last_check,即不含页面的最新一次检查。
{
"success": true,
"data": {
"id": "cmf9k2x1a0001s8h4b7d3q2wz",
"name": "competitor pricing",
"targets": [
{ "url": "https://competitor.com/pricing", "selector": ".pricing-table" }
],
"schedule_cron": "0 * * * *",
"timezone": "UTC",
"notify_emails": [],
"webhook_url": "https://example.com/hooks/crawlforge",
"webhook_secret": "3f9a1c7e5b2d48f0a6c1e9d7b5a3f2c4",
"status": "active",
"next_run_at": "2026-09-07T15:00:00.000Z",
"last_check_at": null,
"retention_days": 30,
"estimated_credits_per_month": 2160,
"created_at": "2026-09-07T14:12:08.000Z",
"updated_at": "2026-09-07T14:12:08.000Z"
}
}列出、立即运行和读取检查
列出
curl "https://crawlforge.dev/api/v1/monitors?limit=20" \
-H "X-API-Key: cf_test_YOUR_KEY"
# Next page: pass the next_cursor from the previous response
curl "https://crawlforge.dev/api/v1/monitors?limit=20&cursor=NEXT_CURSOR" \
-H "X-API-Key: cf_test_YOUR_KEY"立即运行
在计划之外立即运行一次检查,并连同其页面一起返回。适用相同的计费规则。该检查仍在运行时再次调用会返回 409 MONITOR_RUNNING。
# Run one check now and get it back with its pages
curl -X POST https://crawlforge.dev/api/v1/monitors/MONITOR_ID/run \
-H "X-API-Key: cf_test_YOUR_KEY"检查
# Check history (no pages)
curl "https://crawlforge.dev/api/v1/monitors/MONITOR_ID/checks?limit=20" \
-H "X-API-Key: cf_test_YOUR_KEY"
# One check, with its pages and webhook deliveries
curl https://crawlforge.dev/api/v1/monitors/MONITOR_ID/checks/CHECK_ID \
-H "X-API-Key: cf_test_YOUR_KEY"检查对象
一次含两个目标的检查:定价表发生了变更并已计费,文档页面遇到 Cloudflare 挑战因此未计费。deliveries 是该次检查的 webhook 日志。
{
"success": true,
"data": {
"id": "cmf9k5r8t0003s8h4m1n6p4vx",
"monitor_id": "cmf9k2x1a0001s8h4b7d3q2wz",
"started_at": "2026-09-07T15:00:02.000Z",
"finished_at": "2026-09-07T15:00:05.000Z",
"status": "completed",
"summary": { "total": 2, "new": 0, "changed": 1, "unchanged": 0, "blocked": 1, "errored": 0 },
"credits_reserved": 6,
"credits_charged": 3,
"error": null,
"pages": [
{
"url": "https://competitor.com/pricing",
"selector": ".pricing-table",
"status": "changed",
"change_percent": 12.5,
"structural_similarity": 0.9713,
"added_count": 4,
"removed_count": 2,
"added_samples": ["Pro $79 / month", "Includes 50,000 credits"],
"removed_samples": ["Pro $99 / month", "Includes 25,000 credits"],
"content_hash": "9f2c0b7e4d1a8c6f3e5b2a9d7c4f1e8b0a6d3c2f5e9b1a7d4c8f2e6b3a9d1c7f",
"error_code": null,
"error": null,
"blocked_vendor": null,
"duration_ms": 1180,
"charged": true
},
{
"url": "https://competitor.com/docs",
"selector": null,
"status": "blocked",
"change_percent": null,
"structural_similarity": null,
"added_count": null,
"removed_count": null,
"added_samples": [],
"removed_samples": [],
"content_hash": null,
"error_code": "BLOCKED",
"error": "Challenge page served by cloudflare",
"blocked_vendor": "cloudflare",
"duration_ms": 640,
"charged": false
}
],
"deliveries": [
{
"id": "cmf9k5rb20005s8h4q7w2e9rt",
"event": "monitor.page",
"url": "https://example.com/hooks/crawlforge",
"attempts": 1,
"status": "delivered",
"http_status": 200,
"error": null,
"delivered_at": "2026-09-07T15:00:05.100Z"
},
{
"id": "cmf9k5rb20006s8h4z3x8c5vb",
"event": "monitor.check.completed",
"url": "https://example.com/hooks/crawlforge",
"attempts": 1,
"status": "delivered",
"http_status": 200,
"error": null,
"delivered_at": "2026-09-07T15:00:05.400Z"
}
]
}
}计费与月度估算
没有按监控收取的费用,创建、列出、更新或删除监控都不计费。每次检查按每个目标预留 3 credits(即 track_changes 的价格),并对状态为 new、changed 或 unchanged 的每个目标实际扣除 3 credits。结果为 blocked 或 error 的目标不计费。每次检查都会在用量日志中写入一条 track_changes 记录。
如果余额不足以覆盖预留额,该次检查会被记录为 insufficient_credits,并且不会抓取任何内容。
监控上的 estimated_credits_per_month = 计划在未来 30 天内触发的次数 × 目标数 × 3。这是每个目标每次都被抓取并对比的月份的上限。
schedule_cron | 目标数 | 30 天内运行次数 | estimated_credits_per_month |
|---|---|---|---|
0 * * * * | 1 | 720 | 2,160 |
*/15 * * * * | 1 | 2,880 | 8,640 |
0 9 * * * | 5 | 30 | 450 |
检查状态与页面状态
每次检查携带一个 status、一个按页面状态计数的 summary,以及 credits_reserved / credits_charged。
检查 status | 含义 |
|---|---|
running | 检查正在进行中。此期间 POST …/run 返回 409 MONITOR_RUNNING。 |
completed | 所有目标均已处理。请读取 summary 和 pages。 |
failed | 检查未能完成;error 说明原因。 |
skipped_overlap | 该时间槽到来时上一次检查仍在运行。未抓取任何内容。 |
insufficient_credits | 余额不足以覆盖预留额。未抓取任何内容。 |
pages 中的每个条目携带一个 status。changed 页面包含 change_percent、structural_similarity、added_count、removed_count,以及 added_samples 和 removed_samples 中最多 20 条、每条最多 500 个字符的样本行;计数才是真实总数。
页面 status | 含义 | 是否计费 |
|---|---|---|
new | 对该目标的首次捕获。基线已写入;暂时没有可对比的内容。 | 是 |
changed | 内容与上一次检查不同。基线向前滚动到本次捕获。 | 是 |
unchanged | 内容哈希与上一次检查相同。 | 是 |
blocked | 目标返回了挑战墙(Cloudflare、DataDome、PerimeterX、Akamai、Amazon、Vercel)或不可用的文档。blocked_vendor 给出厂商名称。绝不会报告为变更。 | 否 |
error | 抓取失败、选择器未匹配到任何元素,或 robots.txt 禁止该路径。error_code 携带错误码:FETCH_FAILED、SELECTOR_NOT_FOUND、ROBOTS_DISALLOWED 等。 | 否 |
respect_robots 覆盖选项。robots.txt 对 CrawlForge 禁止其路径的目标,在每次检查中都会报告为 error 并带 ROBOTS_DISALLOWED,且绝不计费。计划规则
schedule_cron 是标准的五段式 cron 表达式(分钟、小时、日、月、星期),按 timezone 计算。相邻两次运行至少间隔 5 分钟,因此 * * * * * 和 */2 * * * * 会以 400 拒绝。
CrawlForge 的调度器每 5 分钟唤醒一次,启动所有时间槽已过的检查,因此检查会在其时间槽后的 5 分钟内开始。监控上的 next_run_at 是下一个时间槽;暂停监控会将其置为 null。
如果监控的上一次检查在下一个时间槽到来时仍在运行,则记录一次 skipped_overlap 检查,而不会启动第二次检查。
| 表达式 | 运行时间 |
|---|---|
0 * * * * | 每小时整点(默认值)。 |
*/15 * * * * | 每 15 分钟。 |
0 9 * * 1-5 | 周一至周五 09:00,按 timezone 计算。 |
0 6,18 * * * | 每天 06:00 和 18:00。 |
30 2 1 * * | 每月 1 日 02:30。 |
通知
邮件。 设置了 notify_emails 时,只有当一次检查至少有一个 new、changed、blocked 或 error 页面时才会发送邮件。所有目标都是 unchanged 的检查不会发送任何内容。
Webhook。 设置了 webhook_url 时,每次投递都是一个 POST,JSON 请求体为 { event, id, timestamp, data },并带有以下请求头:
| 请求头 | 值 |
|---|---|
Content-Type | application/json |
X-Webhook-Event | monitor.page 或 monitor.check.completed |
X-Webhook-ID | 投递 id,在重试之间保持不变,因此接收方可以丢弃重复项。 |
X-Webhook-Timestamp | 发送投递时的 Unix 时间,单位毫秒。 |
X-Webhook-Signature | sha256= 后接以 webhook_secret 为密钥、对原始请求体精确计算的十六进制 HMAC-SHA256。 |
两种事件。先发送页面事件,再发送检查摘要。
event | 何时发送 | data |
|---|---|---|
monitor.page | 每个状态不是 unchanged 的页面各发送一条。 | { monitor: { id, name }, check_id, page, dashboard_url }——page 是完整的页面结果,包含样本。 |
monitor.check.completed | 每次完成的检查。 | { monitor, check: { id, started_at, finished_at, status, summary, credits_charged }, pages, dashboard_url }——pages 中的每个条目包含 url, selector, status, change_percent, added_count, removed_count, error_code,不含样本。 |
{
"event": "monitor.check.completed",
"id": "cmf9k5rb20006s8h4z3x8c5vb",
"timestamp": 1788793205400,
"data": {
"monitor": { "id": "cmf9k2x1a0001s8h4b7d3q2wz", "name": "competitor pricing" },
"check": {
"id": "cmf9k5r8t0003s8h4m1n6p4vx",
"started_at": "2026-09-07T15:00:02.000Z",
"finished_at": "2026-09-07T15:00:05.000Z",
"status": "completed",
"summary": { "total": 2, "new": 0, "changed": 1, "unchanged": 0, "blocked": 1, "errored": 0 },
"credits_charged": 3
},
"pages": [
{
"url": "https://competitor.com/pricing",
"selector": ".pricing-table",
"status": "changed",
"change_percent": 12.5,
"added_count": 4,
"removed_count": 2,
"error_code": null
},
{
"url": "https://competitor.com/docs",
"selector": null,
"status": "blocked",
"change_percent": null,
"added_count": null,
"removed_count": null,
"error_code": "BLOCKED"
}
],
"dashboard_url": "https://www.crawlforge.dev/dashboard/monitors/cmf9k2x1a0001s8h4b7d3q2wz"
}
}投递。 最多 4 次尝试(首次加三次重试),间隔分别为 1 秒、2 秒和 4 秒,每次超时 10 秒。2xx 响应视为已投递。除 408 或 429 以外的 4xx 不会重试。投递日志随检查一起保存:GET …/checks/{check_id} 返回 deliveries,包含每个事件的尝试次数、HTTP 状态和错误。
验证签名
在解析 JSON 之前,对原始请求体计算 HMAC,并以恒定时间比较。请求头集合和签名方案与 CrawlForge MCP 服务器的 webhook 完全相同,因此同一个接收端可以同时验证两者。
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
// Sign the RAW body exactly as received. Parsing and re-serialising the JSON
// first changes the bytes and the signature no longer matches.
function verifyCrawlForgeWebhook(rawBody: Buffer, signatureHeader: string, secret: string): boolean {
const expected = Buffer.from('sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'));
const received = Buffer.from(signatureHeader);
return expected.length === received.length && timingSafeEqual(expected, received);
}
const app = express();
// express.raw keeps the body as a Buffer; express.json would have parsed it.
app.post('/hooks/crawlforge', express.raw({ type: 'application/json' }), (req, res) => {
const secret = process.env.CRAWLFORGE_WEBHOOK_SECRET!;
if (!verifyCrawlForgeWebhook(req.body, req.header('X-Webhook-Signature') ?? '', secret)) {
return res.status(401).end();
}
const { event, data } = JSON.parse(req.body.toString('utf8'));
if (event === 'monitor.page') {
console.log(data.page.status, data.page.url, data.page.change_percent);
} else if (event === 'monitor.check.completed') {
console.log('check', data.check.id, data.check.summary);
}
res.status(204).end();
});限制
- 每个账户 50 个监控。创建第 51 个时返回 409
MONITOR_LIMIT_REACHED。 - 每个监控 20 个目标,
notify_emails中 5 个地址。 - 相邻两次运行至少间隔 5 分钟。
retention_days为 1 到 365,默认 30。基线一直保留到监控被删除。- 差异样本:
added_samples和removed_samples各最多 20 行,每行最多 500 个字符。
错误
| 状态 | 含义 |
|---|---|
400 VALIDATION_ERROR | 某个字段超出范围,cron 表达式无效或触发频率高于每 5 分钟一次,或目标 URL 不是公网 http(s) 地址。 |
| 401 | 缺少或无效的 API key。 |
| 404 | 您的账户下没有该 id 对应的监控或检查。 |
409 MONITOR_LIMIT_REACHED | 您已经有 50 个监控。 |
409 MONITOR_RUNNING | 在检查进行中调用了 POST …/run。 |