Monitores alojados
Entregue a CrawlForge un conjunto de páginas y un calendario cron. Las obtiene y compara en su propio programador, registra cada comprobación y le indica qué cambió por correo o mediante un webhook firmado. No hay cuota por monitor: cada comprobación cobra el precio de track_changes por destino comparado.
Descripción general
Un monitor es un nombre, hasta 20 destinos (una url más un selector CSS opcional), un calendario cron de cinco campos con una zona horaria IANA y el lugar al que enviar los resultados. El programador de CrawlForge se ejecuta cada 5 minutos e inicia por sí mismo cada comprobación pendiente, así que no hace falta que nada esté en ejecución de su lado.
La primera comprobación de un destino captura una línea base y la informa como new. Cada comprobación posterior compara con la comprobación anterior y avanza la línea base, de modo que cada comprobación informa el cambio desde la anterior. Las líneas base viven hasta que se elimina el monitor; las comprobaciones se depuran después de retention_days.
Todos los endpoints aceptan el mismo encabezado X-API-Key que las herramientas (o Authorization: Bearer cf_…). Las llamadas de gestión no cobran nada y funcionan con cero credits. Las respuestas usan el envoltorio de las herramientas, { success, data }, y los errores incluyen error.code y error.message.
Endpoints
| Método | Ruta | Qué hace |
|---|---|---|
| POST | /api/v1/monitors | Crea un monitor. Devuelve 201 con el monitor, incluido su webhook_secret. |
| GET | /api/v1/monitors | Lista los monitores, cada uno con su comprobación más reciente como last_check. Parámetros de consulta limit y cursor; el cuerpo incluye next_cursor. Nunca devuelve webhook_secret. |
| GET | /api/v1/monitors/{id} | Un monitor, incluido webhook_secret. |
| PATCH | /api/v1/monitors/{id} | Actualiza cualquier subconjunto de los campos de creación. Poner status en paused deja next_run_at en null. |
| DELETE | /api/v1/monitors/{id} | Elimina el monitor. Devuelve { id, deleted: true }. |
| POST | /api/v1/monitors/{id}/run | Ejecuta una comprobación ahora, en línea, y la devuelve con pages. 409 MONITOR_RUNNING mientras ya haya una comprobación en curso. |
| GET | /api/v1/monitors/{id}/checks | Historial de comprobaciones sin pages. Parámetros de consulta limit y cursor. |
| GET | /api/v1/monitors/{id}/checks/{check_id} | Una comprobación con pages y las deliveries de webhook. |
Crear un monitor
Solo name y targets son obligatorios. Los valores predeterminados le dan una comprobación cada hora en UTC con 30 días de historial.
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"
}'Todos los campos que acepta el cuerpo. PATCH acepta cualquier subconjunto de los mismos campos.
| Campo | Tipo | Predeterminado | Descripción |
|---|---|---|---|
name | string | obligatorio | De 1 a 80 caracteres. |
targets | array | obligatorio | De 1 a 20 objetos { url, selector? }. Las URL deben ser direcciones http(s) públicas; una dirección privada o local se rechaza con 400. |
schedule_cron | string | 0 * * * * | Expresión cron de cinco campos. Las ejecuciones consecutivas deben estar separadas al menos 5 minutos. |
timezone | string | UTC | Zona horaria IANA en la que se evalúa el calendario, por ejemplo Europe/Madrid. |
notify_emails | string[] | — | Hasta 5 direcciones que reciben un correo cuando una comprobación encuentra algo. Consulte Notificaciones. |
webhook_url | string | — | URL https pública que recibe los eventos firmados. Consulte Notificaciones. |
webhook_secret | string | generado | De 16 a 128 caracteres con los que se firma cada entrega. Se genera cuando se omite y webhook_url está definido. Se devuelve al crear y en el GET de uno, nunca en la lista. |
retention_days | number | 30 | De 1 a 365. Las comprobaciones más antiguas se depuran; las líneas base se conservan hasta que se elimina el monitor. |
status | string | active | active o paused. Un monitor en pausa conserva sus líneas base y no tiene next_run_at. |
Cuerpo completo
Dos destinos, mañanas de días laborables en Madrid, correo más webhook con su propio secreto, 90 días de historial:
{
"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"
}El objeto monitor
Crear, el GET de uno y PATCH devuelven el monitor. next_run_at es la próxima franja, last_check_at la hora de la comprobación más reciente y estimated_credits_per_month se explica en Facturación. Las respuestas de lista añaden last_check, la comprobación más reciente sin sus páginas.
{
"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"
}
}Listar, ejecutar ahora y leer comprobaciones
Listar
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"Ejecutar ahora
Ejecuta una comprobación de inmediato, fuera del calendario, y la devuelve con sus páginas. Se aplican las mismas reglas de facturación. Una segunda llamada mientras esa comprobación sigue en curso responde 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"Comprobaciones
# 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"El objeto comprobación
Una comprobación con dos destinos: la tabla de precios cambió y se cobró; la página de documentación se topó con un desafío de Cloudflare y no se cobró. deliveries es el registro de webhooks de la comprobación.
{
"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"
}
]
}
}Facturación y la estimación mensual
No hay cuota por monitor, y crear, listar, actualizar o eliminar un monitor no cuesta nada. Cada comprobación retiene 3 credits por destino, el precio de track_changes, y se queda con 3 por cada destino cuyo estado sea new, changed o unchanged. Los destinos que resultaron blocked o error no se cobran. Cada comprobación escribe una fila track_changes en el registro de uso.
Si su saldo no puede cubrir la retención, la comprobación se registra como insufficient_credits y no se obtiene nada.
estimated_credits_per_month en el monitor es el número de veces que el calendario se dispara en los próximos 30 días × destinos × 3. Es el techo para un mes en el que cada destino se obtiene y compara todas las veces.
schedule_cron | Destinos | Ejecuciones en 30 días | estimated_credits_per_month |
|---|---|---|---|
0 * * * * | 1 | 720 | 2,160 |
*/15 * * * * | 1 | 2,880 | 8,640 |
0 9 * * * | 5 | 30 | 450 |
Estados de comprobación y de página
Cada comprobación lleva un status, un summary con el conteo por estado de página, y credits_reserved / credits_charged.
status de la comprobación | Significado |
|---|---|
running | La comprobación está en curso. Mientras tanto, POST …/run responde 409 MONITOR_RUNNING. |
completed | Se procesaron todos los destinos. Lea summary y pages. |
failed | La comprobación no pudo terminar; error indica por qué. |
skipped_overlap | La comprobación anterior seguía en curso cuando llegó esta franja. No se obtuvo nada. |
insufficient_credits | El saldo no pudo cubrir la retención. No se obtuvo nada. |
Cada entrada de pages lleva un status. Una página changed incluye change_percent, structural_similarity, added_count, removed_count y hasta 20 líneas de muestra de hasta 500 caracteres en added_samples y removed_samples; los conteos son los totales reales.
status de la página | Significado | Se cobra |
|---|---|---|
new | Primera captura de este destino. Se escribió la línea base; todavía no hay nada que comparar. | Sí |
changed | El contenido difiere de la comprobación anterior. La línea base avanza a esta captura. | Sí |
unchanged | Mismo hash de contenido que la comprobación anterior. | Sí |
blocked | El destino respondió con un muro de desafío (Cloudflare, DataDome, PerimeterX, Akamai, Amazon, Vercel) o con un documento inutilizable. blocked_vendor lo nombra. Nunca se informa como un cambio. | No |
error | La obtención falló, el selector no coincidió con nada o robots.txt no permite la ruta. error_code lleva el código: FETCH_FAILED, SELECTOR_NOT_FOUND, ROBOTS_DISALLOWED, etc. | No |
respect_robots. Un destino cuya ruta robots.txt no permite a CrawlForge se informa como error con ROBOTS_DISALLOWED en cada comprobación y nunca se cobra.Reglas del calendario
schedule_cron es una expresión cron estándar de cinco campos (minuto, hora, día del mes, mes, día de la semana) evaluada en timezone. Dos ejecuciones consecutivas deben estar separadas al menos 5 minutos, así que * * * * * y */2 * * * * se rechazan con 400.
El programador de CrawlForge se despierta cada 5 minutos e inicia todas las comprobaciones cuya franja ya pasó, de modo que una comprobación comienza dentro de los 5 minutos siguientes a su franja. next_run_at en el monitor es la próxima franja; pausar el monitor lo deja en null.
Un monitor cuya comprobación anterior sigue en curso cuando llega su siguiente franja registra una comprobación skipped_overlap en lugar de iniciar una segunda.
| Expresión | Se ejecuta |
|---|---|
0 * * * * | Cada hora, en punto (el valor predeterminado). |
*/15 * * * * | Cada 15 minutos. |
0 9 * * 1-5 | A las 09:00 de lunes a viernes, en timezone. |
0 6,18 * * * | A las 06:00 y a las 18:00 todos los días. |
30 2 1 * * | A las 02:30 del primer día de cada mes. |
Notificaciones
Correo. Cuando notify_emails está definido, solo se envía un mensaje por una comprobación que tenga al menos una página new, changed, blocked o error. Una comprobación en la que todos los destinos son unchanged no envía nada.
Webhooks. Cuando webhook_url está definido, cada entrega es un POST con un cuerpo JSON { event, id, timestamp, data } y estos encabezados:
| Encabezado | Valor |
|---|---|
Content-Type | application/json |
X-Webhook-Event | monitor.page o monitor.check.completed |
X-Webhook-ID | Id de la entrega, estable entre reintentos, para que un receptor pueda descartar un duplicado. |
X-Webhook-Timestamp | Hora Unix en milisegundos en la que se envió la entrega. |
X-Webhook-Signature | sha256= seguido del HMAC-SHA256 hexadecimal del cuerpo sin procesar exacto, con webhook_secret como clave. |
Dos eventos. Primero salen los eventos de página y después el resumen de la comprobación.
event | Cuándo | data |
|---|---|---|
monitor.page | Uno por cada página cuyo estado no sea unchanged. | { monitor: { id, name }, check_id, page, dashboard_url }: page es el resultado completo de la página, muestras incluidas. |
monitor.check.completed | Cada comprobación completada. | { monitor, check: { id, started_at, finished_at, status, summary, credits_charged }, pages, dashboard_url }: cada entrada de pages tiene url, selector, status, change_percent, added_count, removed_count, error_code, sin muestras. |
{
"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"
}
}Entrega. Hasta 4 intentos (el primero más tres reintentos) con 1 s, 2 s y 4 s entre ellos y un tiempo de espera de 10 s cada uno. Una respuesta 2xx cuenta como entregada. Un 4xx distinto de 408 o 429 no se reintenta. El registro de entregas viaja con la comprobación: GET …/checks/{check_id} devuelve deliveries con el número de intentos, el estado HTTP y el error de cada evento.
Verificar la firma
Calcule el HMAC sobre el cuerpo sin procesar de la solicitud, antes de cualquier análisis JSON, y compárelo en tiempo constante. El conjunto de encabezados y el esquema de firma son los mismos que usan los webhooks del servidor MCP de CrawlForge, así que un solo receptor verifica ambos.
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();
});Límites
- 50 monitores por cuenta. La creación número 51 responde 409
MONITOR_LIMIT_REACHED. - 20 destinos por monitor y 5 direcciones en
notify_emails. - Al menos 5 minutos entre ejecuciones consecutivas.
retention_daysde 1 a 365, 30 de forma predeterminada. Las líneas base se conservan hasta que se elimina el monitor.- Muestras de diferencias: hasta 20 líneas de hasta 500 caracteres en cada uno de
added_samplesyremoved_samples.
Errores
| Estado | Significado |
|---|---|
400 VALIDATION_ERROR | Un campo está fuera de rango, la expresión cron no es válida o se dispara con más frecuencia que cada 5 minutos, o una URL de destino no es una dirección http(s) pública. |
| 401 | API key ausente o inválida. |
| 404 | No hay ningún monitor ni comprobación con ese id en su cuenta. |
409 MONITOR_LIMIT_REACHED | Ya tiene 50 monitores. |
409 MONITOR_RUNNING | POST …/run mientras hay una comprobación en curso. |