API de Detección de Cambios en Sitios Web#

Beta privada. Watch ya se puede llamar hoy con tu clave de API habitual en cualquier plan de pago, y los watchers no consumen ops; el plan gratuito Founding no puede crearlos. No está anunciado ni disponible de forma general: las formas de la solicitud y de la respuesta pueden cambiar sin avisar, y no hay ningún compromiso de estabilidad ni de soporte, así que todavía no montes nada crítico encima. La hoja de ruta está en Próximamente, y cada versión se anuncia en el changelog.

POST /v2/watch es una API de detección de cambios en sitios web: registras una página una sola vez, y un planificador local del droplet la vuelve a renderizar con una cadencia fija en Chrome headless real, compara cada captura con la anterior y te notifica (mediante webhook firmado con HMAC, email, o ambos) cuando la página realmente cambia. Reemplazará el cron job más el diffing más el entramado de alertas que de otro modo tendrías que montar en torno a el endpoint de perceive: un solo registro en lugar de un planificador, un bucket de almacenamiento y un script de comparación.

Esta es la llamada mínima útil. Envía una URL y recibes de vuelta un watcher programado para verificar cada hora:

curl -X POST https://api.enconvert.com/v2/watch \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing"
  }'

La respuesta es el registro completo del watcher. Está active de inmediato, y next_check_at se fija en el siguiente tick del poller:

{
    "watcher_id": "wat_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7",
    "url": "https://example.com/pricing",
    "status": "active",
    "frequency_minutes": 60,
    "diff_mode": "auto",
    "track_fields": null,
    "webhook_url": null,
    "notify_email": true,
    "consecutive_errors": 0,
    "checks_count": 0,
    "last_check_at": null,
    "next_check_at": "2026-06-24T18:31:07Z",
    "last_change_at": null,
    "created_at": "2026-06-24T18:31:07Z",
    "updated_at": null
}

Endpoints#

Método Ruta Propósito
POST /v2/watch Crea un watcher para una URL. Devuelve 201.
GET /v2/watch Lista los watchers de este proyecto, del más reciente al más antiguo.
GET /v2/watch/{watcher_id} Obtiene el registro completo de un watcher.
GET /v2/watch/{watcher_id}/snapshots Lista el historial de verificaciones de un watcher, del más reciente al más antiguo.
PATCH /v2/watch/{watcher_id} Actualiza la cadencia, la configuración de diff, o pausa/reanuda.
DELETE /v2/watch/{watcher_id} Elimina un watcher (soft-delete, idempotente).

Content-Type: application/json en POST y PATCH.


Autenticación#

Autentica con una clave privada en el encabezado X-API-Key para llamadas de servidor a servidor. Este es el flujo que usan todos los ejemplos a continuación.

X-API-Key: sk_your_private_key

Las claves públicas con un token JWT bearer también funcionan, usando el mismo flujo que todos los demás endpoints: genera un token con tu clave pk_ y luego envíalo como Authorization: Bearer <token>. El flujo completo, incluido el bloqueo de dominio y la renovación de tokens, está en la guía de autenticación.

Cada clave de API lleva una allowlist de endpoints permitidos. Si /v2/watch no está en la lista de la clave, la solicitud de creación se rechaza con 403. Una vez que una clave puede crear watchers, también puede acceder a las rutas por watcher que le pertenecen: GET, PATCH y DELETE en /v2/watch/{watcher_id} y su página /snapshots se permiten automáticamente, así que no tienes que incluir cada verbo por separado en la allowlist.


Cómo funciona watch#

No hay ninguna cola externa detrás de esto: ni Google Cloud Tasks, ni un planificador de terceros. El cronograma vive por completo en la columna de base de datos next_check_at, y un poller interno del proceso lo controla:

  1. Crear. Haces un POST de una URL. La URL se filtra contra SSRF (un host privado, loopback o de metadatos se rechaza antes de escribir cualquier fila), se genera un ID wat_, y el watcher se guarda como active con next_check_at fijado en el momento actual.
  2. Reclamar. Un watch_worker local del droplet escanea cada 60 segundos en busca de filas activas cuyo next_check_at ya haya pasado. Las reclama con FOR UPDATE SKIP LOCKED y avanza el cronograma de cada una un intervalo completo dentro de la misma transacción, así que un renderizado lento nunca se reclama dos veces y un fallo a mitad del renderizado simplemente salta un ciclo.
  3. Renderizar. Cada watcher reclamado se renderiza una vez a través del singleton compartido de Chrome headless, el mismo pipeline de captura que hay detrás de el endpoint de perceive. El renderizado no usa credenciales: no se almacenan auth, cookies ni encabezados, así que no queda nada secreto en reposo para la verificación recurrente.
  4. Puntuar y comparar. Un renderizado con una puntuación por debajo del piso de calidad (0.4) o marcado como bloqueado se registra solo como verificación de auditoría, sin hash de contenido, por lo que nunca se convierte en línea base de diff ni dispara una notificación. Un buen renderizado se convierte en una captura (texto del contenido principal más la estructura extraída), se compara contra la última captura buena, y el veredicto se escribe en una fila de snapshot.
  5. Notificar. Cuando el diff reporta un cambio, el webhook firmado con HMAC (si está configurado) y el email del propietario (si notify_email está activo) se disparan simultáneamente, con la mejor entrega posible.
  6. Reprogramar. El worker escribe el siguiente next_check_at. Tres fallos de renderizado consecutivos pausan el watcher y envían un email al propietario en lugar de reprogramar.

Como el cronograma es una columna de base de datos, el tiempo de inactividad no necesita lógica de recuperación: el primer tick tras el arranque recoge todo lo que estaba atrasado.


Parámetros de la solicitud#

Crear (POST /v2/watch)#

Parámetro Tipo Predeterminado Descripción
url string ninguno La página a monitorear. Debe comenzar con http:// o https://. Máximo 2,048 caracteres. Obligatorio.
frequency_minutes integer 60 Minutos entre verificaciones. Piso horario estricto: mínimo 60, máximo 43200 (30 días).
diff_mode string "auto" Qué estrategia de diff aplicar. auto, text, structured, tables, o metadata. Consulta Modos de diff.
track_fields object null Subconjunto opcional de campos/selectores para acotar qué cuenta como cambio. Consulta Rastrear un subconjunto de campos.
webhook_url string null Destino opcional para la notificación de cambios. Firmado con HMAC, filtrado contra SSRF justo antes de cada entrega. Máximo 2,048 caracteres; debe ser http(s).
notify_email boolean true Envía un email al propietario del proyecto cuando se detecta un cambio y en caso de auto-pausa.

El esquema de la solicitud es estricto (extra="forbid"): un campo desconocido se rechaza con 422. Deliberadamente no hay superficie para auth, cookies ni headers aquí, porque los watchers permanecen sin credenciales, la misma postura que el endpoint de ingest.

Actualizar (PATCH /v2/watch/{watcher_id})#

Todos los campos son opcionales; solo se aplican las claves presentes en el cuerpo.

Parámetro Tipo Descripción
frequency_minutes integer Nueva cadencia. Los mismos límites 6043200 que en la creación.
diff_mode string Cambia la estrategia de diff.
track_fields object Reemplaza el subconjunto de campos rastreados.
webhook_url string Establece un nuevo webhook. Una cadena vacía es la señal explícita de "borrarlo" y almacena NULL.
notify_email boolean Activa o desactiva el email al propietario.
status string active o paused. Reanudar rearma el cronograma (next_check_at se fija en el siguiente tick); pausar lo limpia para que el poller deje de reclamar la fila.

Un cuerpo vacío ({}) se rechaza con 422 en lugar de ignorarse silenciosamente. Ten en cuenta que aquí status solo acepta active o paused. El estado terminal deleted se alcanza mediante DELETE, nunca mediante PATCH. Reanudar un watcher pausado cuenta como agregar un monitor activo, así que vuelve a comprobar el mismo límite de max_watchers que la creación y puede devolver 402.


Modos de diff#

diff_mode elige cuál de las cuatro estrategias, sensibles al tipo de contenido, ejecuta el motor. auto ejecuta las cuatro y combina sus resultados; los modos con nombre restringen el diff a una sola estrategia.

diff_mode Estrategia Qué señala
auto (predeterminado) Las cuatro siguientes Todo tipo de cambio en una sola pasada.
text Ratio SequenceMatcher del contenido principal El texto del cuerpo cambió, se señala cuando la similitud cae por debajo de 0.98, de modo que una palabra reordenada o un cambio de espacios en blanco no hace flapear al watcher. Incluye un diff unificado limitado a 100 líneas.
structured Coincidencia de listas con clave Elementos agregados / eliminados / modificados por campo entre enlaces (emparejados por href) y bloques JSON-LD (emparejados por @type + name). No depende del orden.
tables Coincidencia por encabezado de contexto Las tablas se emparejan por título/encabezado; reporta cambios en el número de filas, tablas agregadas/eliminadas, y ediciones de contenido con el mismo número de filas (limitado a 100 filas de contexto).
metadata Comparación de diccionario clave por clave Campos de metadatos de página agregados, eliminados y modificados.

Sin importar qué modo elijas, el similarity del snapshot siempre es el ratio general de toda la captura (0.0–1.0). En un modo restringido eso significa que similarity puede mostrarse bajo mientras has_changes es false, porque una sección que no estás comparando se movió mientras nada cambió en la estrategia que elegiste.

Rastrear un subconjunto de campos#

track_fields acota el diff a los cambios cuya sección, campo o clave coincide con un término rastreado. Acepta un objeto cuyas claves, más cualquier valor de lista, se convierten en los términos rastreados. Así, {"metadata": ["title"]} rastrea tanto la sección metadata como el campo title. La coincidencia es por token completo, no por subcadena: un término price coincide con offers.price pero no con priceCurrency.


Respuesta#

POST, GET /v2/watch/{watcher_id}, PATCH y DELETE devuelven todos el mismo objeto watcher.

Campo Tipo Descripción
watcher_id string ID opaco (wat_...). Úsalo en las rutas por watcher.
url string La URL monitoreada.
status string active, paused, o deleted.
frequency_minutes integer Cadencia actual, después del piso horario.
diff_mode string La estrategia de diff activa.
track_fields object El subconjunto de campos rastreados, o null.
webhook_url string El webhook de cambios, o null.
notify_email boolean Si el email al propietario está activo.
consecutive_errors integer Fallos de renderizado consecutivos. Se reinicia a 0 en una verificación exitosa; al llegar a 3 el watcher se auto-pausa.
checks_count integer Total de verificaciones ejecutadas, exitosas o fallidas.
last_check_at string Marca de tiempo UTC de la verificación más reciente, o null.
next_check_at string Marca de tiempo UTC de la próxima verificación programada. null mientras está pausado o eliminado.
last_change_at string Marca de tiempo UTC del cambio detectado más reciente, o null.
created_at string Cuándo se creó el watcher.
updated_at string Última modificación, o null si nunca se actualizó.

GET /v2/watch devuelve un WatcherSummary compacto por fila (omite diff_mode, track_fields, webhook_url, notify_email y updated_at) envuelto en un sobre de paginación:

Campo Tipo Descripción
watchers array La página de resúmenes, del más reciente al más antiguo.
skip integer El offset que solicitaste.
limit integer El tamaño de página en vigor.
has_more boolean true cuando existen más watchers más allá de esta página.

Historial de snapshots#

GET /v2/watch/{watcher_id}/snapshots devuelve la línea de tiempo de verificaciones del watcher, de la más reciente a la más antigua. Cada entrada lleva el veredicto del diff. El cuerpo de la captura del snapshot en sí vive en el almacenamiento y no se devuelve aquí.

Campo Tipo Descripción
checked_at string Marca de tiempo UTC de la verificación.
has_changes boolean Si esta verificación detectó un cambio.
similarity number Ratio general de toda la captura, 0.0–1.0, o null.
render_quality number Puntuación de calidad de renderizado de la verificación, o null.
change_count integer Número de registros de cambio estructurados.
changes array El diff estructurado: un registro por cambio, cada uno con section, kind (added/removed/modified), key, field, before, after.

Vale la pena aclarar. Los valores before y after dentro de changes son contenido de página sin procesar (texto de enlaces, metadatos, valores JSON-LD), no una salida saneada. Si los renderizas en HTML (un dashboard, un email), debes escaparlos tú mismo. Los valores de cadena largos ya vienen truncados a 2,000 caracteres, y un solo diff está limitado a 500 registros de cambio.


Ciclo de vida y programación#

Un watcher atraviesa tres estados:

  • active: en el cronograma del poller. next_check_at está definido.
  • paused: fuera del cronograma (next_check_at es null). Se alcanza mediante PATCH {"status": "paused"} o automáticamente después de tres fallos de renderizado consecutivos.
  • deleted: lápida terminal, alcanzada solo mediante DELETE. La fila se conserva (para que el historial de verificaciones sobreviva) pero nunca aparece en las listas y se lee como 404 en las rutas por watcher.

El piso horario se aplica en dos lugares: al crear/actualizar, y otra vez por el planificador cuando avanza next_check_at. Así, incluso una fila cuya cadencia se editó directamente en la base de datos nunca puede superar el piso.

Auto-pausa#

Después de tres fallos de renderizado consecutivos, el watcher se pone en paused, se limpia su cronograma, y el propietario recibe un email de watcher pausado si notify_email está activo. Una sola verificación exitosa reinicia consecutive_errors a 0. Para reiniciar un watcher pausado, hazle un PATCH de vuelta a active, lo cual rearma next_check_at para el siguiente tick.

Eliminación#

DELETE /v2/watch/{watcher_id} es un soft-delete: cambia status a deleted, limpia next_check_at, y devuelve el registro con lápida con un 200. Es idempotente, así que eliminar un watcher ya eliminado devuelve el mismo registro sin cambios. Los watchers eliminados dejan de contar contra tu límite de max_watchers de inmediato (solo cuentan los watchers active).


Ejemplos de código#

curl: crear con valores predeterminados#

curl -X POST https://api.enconvert.com/v2/watch \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing"
  }'

curl: cada 6 horas, webhook más rastreo de tablas#

curl -X POST https://api.enconvert.com/v2/watch \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "frequency_minutes": 360,
    "diff_mode": "tables",
    "webhook_url": "https://hooks.example.com/enconvert",
    "notify_email": false
  }'

curl: pausar y luego reanudar#

curl -X PATCH \
  https://api.enconvert.com/v2/watch/wat_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{"status": "paused"}'

curl -X PATCH \
  https://api.enconvert.com/v2/watch/wat_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'

Python#

import requests

BASE = "https://api.enconvert.com"
HEADERS = {"X-API-Key": "sk_your_private_key"}

# Create a watcher.
created = requests.post(
    f"{BASE}/v2/watch",
    headers=HEADERS,
    json={
        "url": "https://example.com/pricing",
        "frequency_minutes": 120,
        "diff_mode": "auto",
    },
)
created.raise_for_status()
watcher = created.json()
watcher_id = watcher["watcher_id"]

# Later: pull the check history and read the diff verdicts.
snaps = requests.get(
    f"{BASE}/v2/watch/{watcher_id}/snapshots",
    headers=HEADERS,
)
snaps.raise_for_status()
for snap in snaps.json()["snapshots"]:
    if snap["has_changes"]:
        print(snap["checked_at"], snap["change_count"], snap["similarity"])

Node.js#

const BASE = "https://api.enconvert.com";
const HEADERS = {
    "Content-Type": "application/json",
    "X-API-Key": "sk_your_private_key"
};

// Create a watcher.
const created = await fetch(`${BASE}/v2/watch`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
        url: "https://example.com/pricing",
        frequency_minutes: 120,
        diff_mode: "auto"
    })
});
const watcher = await created.json();

// List this project's watchers, newest first.
const list = await fetch(`${BASE}/v2/watch?limit=20`, {
    headers: { "X-API-Key": "sk_your_private_key" }
}).then(r => r.json());

console.log(watcher.watcher_id, list.has_more);

Respuestas de error#

Estado Condición
400 Bad Request La URL resuelve a una dirección privada, loopback, link-local o de metadatos (protección SSRF al crear).
401 Unauthorized Falta la clave de API / token JWT, o es inválida.
402 Payment Required Watch no está habilitado en tu plan, o tu número de watchers activos ha alcanzado max_watchers (también se aplica al reanudar de paused→active).
403 Forbidden /v2/watch no está en los endpoints permitidos de la clave de API.
404 Not Found watcher_id desconocido, uno perteneciente a otro proyecto, o uno ya eliminado (soft-delete).
422 Unprocessable Entity frequency_minutes por debajo de 60 o por encima de 43200, un cuerpo PATCH vacío ({}), un enum inválido en diff_mode o status, una url/webhook_url que no es http(s), o cualquier campo desconocido.
500 Internal Server Error No se pudo crear el watcher. Reinténtalo.

La referencia completa de códigos de estado está en la guía de códigos de error.


Límites#

Límite Valor
Longitud de la URL 2,048 caracteres
Watchers activos (max_watchers) 20 / 100 / 500 en Indie / Studio / Production; watch no está disponible en Founding
Ops por comprobación 0 (los watchers nunca consumen la cuota mensual de ops)
Longitud de webhook_url 2,048 caracteres
frequency_minutes 60–43,200 (1 hora a 30 días)
Piso horario 60 minutos, aplicado al crear, actualizar y programar
Intervalo de sondeo 60 segundos (una verificación se dispara dentro del minuto de su hora programada)
Errores consecutivos antes de auto-pausa 3
Piso de calidad de renderizado (sin diff por debajo) 0.4
Umbral de similitud para cambios de texto 0.98
Diff de texto unificado limitado a 100 líneas
Registros de cambio por diff limitado a 500
Valor de cadena por cambio truncado a 2,000 caracteres
Cuerpo de texto capturado comparado limitado a 200,000 caracteres
Tamaño de página de lista / snapshot predeterminado 20, máximo 100

Preguntas frecuentes#

¿Cómo configuro webhooks para el monitoreo de cambios en un sitio?#

Pasa webhook_url al crear el watcher con POST /v2/watch, o agrégalo después mediante PATCH /v2/watch/{watcher_id}. Cuando una verificación detecta un cambio, EnConvert envía un POST firmado con HMAC a esa URL; el destino se filtra contra SSRF justo antes de cada entrega, y una cadena vacía en PATCH lo borra.

¿Con qué frecuencia puede la API verificar cambios en una página?#

frequency_minutes define la cadencia, de 60 (el piso horario estricto) a 43200 (30 días). El poller escanea cada 60 segundos, así que una verificación se dispara dentro del minuto de su hora programada.

¿Por qué mi watcher se pausó solo?#

Tres fallos de renderizado consecutivos auto-pausan un watcher, limpian su cronograma y envían un email al propietario si notify_email está activo. Hazle un PATCH de vuelta con {"status": "active"} para rearmar next_check_at; una sola verificación exitosa reinicia consecutive_errors a 0.

¿Puedo monitorear solo parte de una página, como un precio o una tabla?#

Sí. track_fields acota el diff a los cambios cuya sección, campo o clave coincide con un término rastreado (coincidencia por token completo, así que price coincide con offers.price pero no con priceCurrency), y diff_mode puede restringir la detección a una sola estrategia como tables, structured, text, o metadata.