Watch

POST /v2/watch convierte cualquier URL en un monitor de cambios. Registras una página una vez; 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 —por webhook, correo o ambos— cuando la página cambia de verdad. Reemplaza la maquinaria de cron, comparación y alertas que de otro modo tendrías que montar alrededor de el endpoint perceive: un solo registro en lugar de un planificador, un bucket de almacenamiento y un script de comparación.

Esta es la llamada útil más pequeña. Envía una URL y recibe de vuelta un watcher programado para comprobar cada hora:

curl -X POST https://api.enconvert.com/v2/watch \
  -H "X-API-Key: sk_live_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
}

Conviene señalarlo de entrada. /v2/watch es un endpoint V2 con su propia verificación de plan, separada de las conversiones V1. Cuántos watchers activos puedes mantener a la vez está limitado por max_watchers en tu plan, y un plan con watch deshabilitado no puede crear ninguno en absoluto: ambas denegaciones vuelven como 402. Consulta la página de precios para ver las asignaciones actuales por nivel.


Endpoints

Method Path Propósito
POST /v2/watch Crea un watcher para una URL. Devuelve 201.
GET /v2/watch Lista los watchers de este proyecto, los más recientes primero.
GET /v2/watch/{watcher_id} Obtiene el registro completo de un watcher.
GET /v2/watch/{watcher_id}/snapshots Lista el historial de comprobaciones de un watcher, los más recientes primero.
PATCH /v2/watch/{watcher_id} Actualiza la cadencia, los ajustes de diff o pausa/reanuda.
DELETE /v2/watch/{watcher_id} Borrado lógico de un watcher (idempotente).

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


Autenticación

Autentícate con una clave privada en la cabecera X-API-Key para llamadas de servidor a servidor. Esta es la vía que usa cada ejemplo de abajo.

X-API-Key: sk_live_your_private_key

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

Cada clave de API lleva una lista 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 alcanzar las rutas por watcher que posee: 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 en la lista por separado.


Cómo funciona watch

No hay ninguna cola externa detrás de esto: ni Google Cloud Tasks, ni un planificador de terceros. La programación vive enteramente en la columna de base de datos next_check_at, y un poller en proceso la impulsa:

  1. Create. Haces un POST de una URL. La URL se filtra por SSRF (un host privado, de loopback o de metadatos se rechaza antes de escribir ninguna fila), se acuña un ID wat_, y el watcher se almacena como active con next_check_at fijado en el momento actual.
  2. Claim. Un watch_worker local del droplet escanea cada 60 segundos en busca de filas activas cuyo next_check_at ya haya pasado. Las reclama bajo FOR UPDATE SKIP LOCKED y avanza la programación de cada una un intervalo completo en la misma transacción: así un renderizado lento nunca se reclama dos veces, y un fallo a mitad de renderizado simplemente salta un ciclo.
  3. Render. 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 perceive. El renderizado es sin credenciales: no se almacena auth, cookies ni cabeceras, así que no queda nada secreto en reposo para la comprobación recurrente.
  4. Score and diff. Un renderizado que puntúa por debajo del umbral mínimo de calidad (0.4) o marcado como bloqueado se registra como una comprobación solo de auditoría: sin hash de contenido, así que nunca se convierte en una línea base de diff ni dispara una notificación. Un buen renderizado se convierte en una captura (texto del contenido principal más estructura extraída), se compara con la última captura buena, y el veredicto se escribe en una fila de snapshot.
  5. Notify. Cuando el diff reporta un cambio, el webhook firmado con HMAC (si está configurado) y el correo del propietario (si notify_email está activado) se disparan de forma concurrente, con mejor esfuerzo.
  6. Reschedule. El worker escribe el siguiente next_check_at. Tres fallos de renderizado consecutivos pausan el watcher y envían un correo al propietario en lugar de reprogramar.

Como la programación es una columna de base de datos, el tiempo de inactividad no necesita lógica de recuperación: el primer tick tras el arranque barre todo lo que esté vencido.


Parámetros de la solicitud

Create (POST /v2/watch)

Parameter Type Default Descripción
url string La página a monitorizar. Debe empezar por http:// o https://. Máximo 2.048 caracteres. Obligatorio.
frequency_minutes integer 60 Minutos entre comprobaciones. Suelo 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 campo/selector para acotar qué cuenta como cambio. Consulta Rastrear un subconjunto de campos.
webhook_url string null Destino opcional de notificación de cambios. Firmado con HMAC, filtrado por SSRF justo antes de cada entrega. Máximo 2.048 caracteres; debe ser http(s).
notify_email boolean true Envía correo al propietario del proyecto ante un cambio detectado y ante una autopausa.

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

Update (PATCH /v2/watch/{watcher_id})

Cada campo es opcional; solo se aplican las claves presentes en el cuerpo.

Parameter Type Descripción
frequency_minutes integer Nueva cadencia. Los mismos límites 6043200 que en create.
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 "bórralo" y almacena NULL.
notify_email boolean Alterna el correo al propietario.
status string active o paused. Reanudar rearma la programación (next_check_at se fija en el siguiente tick); pausar la borra para que el poller deje de reclamar la fila.

Un cuerpo vacío ({}) se rechaza con 422 en lugar de tratarse como un no-op silencioso. Ten en cuenta que status aquí solo acepta active o paused: el estado terminal deleted se alcanza mediante DELETE, nunca con PATCH. Reanudar un watcher en pausa cuenta como añadir un monitor activo, así que vuelve a verificar el mismo límite max_watchers que en create y puede devolver 402.


Modos de diff

diff_mode elige cuál de las cuatro estrategias conscientes del tipo de contenido ejecuta el motor. auto ejecuta las cuatro y fusiona sus hallazgos; los modos con nombre restringen el diff a una sola estrategia.

diff_mode Estrategia Qué señala
auto (por defecto) Las cuatro de abajo Todo tipo de cambio en una sola pasada.
text Ratio de SequenceMatcher del contenido principal Cambió el texto del cuerpo: se señala cuando la similitud baja de 0.98, así que reordenar una palabra o editar espacios en blanco no hace oscilar el watcher. Lleva un diff unificado limitado a 100 líneas.
structured Coincidencia de listas con clave Elementos añadidos / eliminados / modificados por campo a través de enlaces (emparejados por href) y bloques JSON-LD (emparejados por @type + name). Insensible al orden.
tables Coincidencia por encabezado de contexto Tablas emparejadas por título/encabezado; reporta cambios en el número de filas, tablas añadidas/eliminadas y ediciones de contenido con igual número de filas (acotado a 100 filas de contexto).
metadata Comparación de diccionario clave por clave Campos de metadatos de página añadidos, eliminados y modificados.

Sea cual sea el modo que elijas, la similarity del snapshot es siempre el ratio global de la captura completa (0.0–1.0). Bajo un modo restringido eso significa que similarity puede leerse baja mientras has_changes es false: una sección que no estás comparando se desvió, pero nada en la estrategia que elegiste cambió.

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: sus 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.

Field Type Descripción
watcher_id string ID opaco (wat_...). Úsalo en las rutas por watcher.
url string La URL monitorizada.
status string active, paused o deleted.
frequency_minutes integer Cadencia actual, tras el suelo 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 correo al propietario está activado.
consecutive_errors integer Fallos de renderizado seguidos. Se reinicia a 0 en una comprobación exitosa; en 3 el watcher se autopausa.
checks_count integer Total de comprobaciones ejecutadas, exitosas o fallidas.
last_check_at string Marca de tiempo UTC de la comprobación más reciente, o null.
next_check_at string Marca de tiempo UTC de la siguiente comprobación programada. null mientras está en pausa 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 mutació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 página:

Field Type Descripción
watchers array La página de resúmenes, los más recientes primero.
skip integer El desplazamiento que solicitaste.
limit integer El tamaño de página en efecto.
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 comprobaciones del watcher, los más recientes primero. Cada entrada lleva el veredicto del diff: el cuerpo de la captura del snapshot en sí vive en almacenamiento y no se devuelve aquí.

Field Type Descripción
checked_at string Marca de tiempo UTC de la comprobación.
has_changes boolean Si esta comprobación detectó un cambio.
similarity number Ratio global de la captura completa, 0.0–1.0, o null.
render_quality number Puntuación de calidad de renderizado de la comprobació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.

Conviene señalarlo. Los valores before y after dentro de changes son contenido de página crudo (texto de enlace, metadatos, valores JSON-LD), no salida saneada. Si los renderizas en HTML —un dashboard, un correo— debes escaparlos tú mismo. Los valores de cadena largos ya están truncados a 2.000 caracteres, y un solo diff está limitado a 500 registros de cambio.


Ciclo de vida y programación

Un watcher pasa por tres estados:

  • active — en la programación del poller. next_check_at está fijado.
  • paused — fuera de la programación (next_check_at es null). Se alcanza ya sea con PATCH {"status": "paused"} o automáticamente tras tres fallos de renderizado consecutivos.
  • deleted — lápida terminal, alcanzada solo vía DELETE. La fila se conserva (para que sobreviva el historial de comprobaciones) pero nunca aparece en listas y se lee como 404 en las rutas por watcher.

El suelo horario se aplica en dos lugares: en create/update, y de nuevo por el planificador cuando avanza next_check_at. Así que incluso una fila cuya cadencia se editó directamente en la base de datos nunca puede superar el suelo.

Autopausa

Tras tres fallos de renderizado consecutivos, el watcher se pone en paused, su programación se borra y —si notify_email está activado— el propietario recibe un correo de watcher en pausa. Una sola comprobación exitosa reinicia consecutive_errors a 0. Para reiniciar un watcher en pausa, hazle un PATCH de vuelta a active, lo que rearma next_check_at para el siguiente tick.

Eliminación

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


Ejemplos de código

curl — crear con valores por defecto

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

curl — cada 6 horas, webhook + rastreo de tablas

curl -X POST https://api.enconvert.com/v2/watch \
  -H "X-API-Key: sk_live_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, luego reanudar

curl -X PATCH \
  https://api.enconvert.com/v2/watch/wat_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
  -H "X-API-Key: sk_live_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_live_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'

Python

import requests

BASE = "https://api.enconvert.com"
HEADERS = {"X-API-Key": "sk_live_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_live_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_live_your_private_key" }
}).then(r => r.json());

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

Verificación de plan

Watch está limitado de dos maneras, ambas separadas de tu cuota de conversiones V1:

  • watch_enabled — una bandera booleana del plan. Un plan sin ella no puede crear un watcher en absoluto (402).
  • max_watchers — el límite de cuántos watchers active puede mantener un proyecto a la vez. Los watchers en pausa y eliminados no cuentan. Un valor de 0 con la bandera activada significa ilimitado (enterprise/admin).

Crear un watcher no consume una conversión ni una operación V2: watch está limitado por cuántos watchers mantienes, no por un contador por llamada. Como cada watcher comprueba con una cadencia de una hora o más lenta, el volumen total de comprobaciones de un proyecto está acotado por max_watchers y el suelo horario juntos. Las asignaciones actuales de max_watchers por plan están en la página de precios.


Respuestas de error

Status Condición
400 Bad Request La URL resuelve a una dirección privada, de loopback, de enlace local o de metadatos (protección SSRF en el momento de la creación).
401 Unauthorized Clave de API / token JWT ausente o inválido.
402 Payment Required Watch no está habilitado en tu plan, o tu recuento de watchers activos ha alcanzado max_watchers (también se aplica en una reanudación paused→active).
403 Forbidden /v2/watch no está en los endpoints permitidos de la clave de API.
404 Not Found watcher_id desconocido, uno propiedad de otro proyecto, o uno ya borrado lógicamente.
422 Unprocessable Entity frequency_minutes por debajo de 60 o por encima de 43200, un cuerpo PATCH vacío ({}), un enum incorrecto en diff_mode o status, una url/webhook_url que no es http(s), o cualquier campo desconocido.
500 Internal Server Error El watcher no se pudo crear. Reintenta.

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 URL 2.048 caracteres
Longitud de webhook_url 2.048 caracteres
frequency_minutes 60–43.200 (1 hora a 30 días)
Suelo horario 60 minutos, aplicado en create, update y programación
Intervalo de sondeo 60 segundos (una comprobación se dispara dentro de un minuto de su hora programada)
Errores consecutivos antes de autopausa 3
Suelo de calidad de renderizado (sin diff por debajo) 0.4
Umbral de similitud de cambio de texto 0.98
Diff unificado de texto 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 / snapshots por defecto 20, máximo 100
Watchers activos por proyecto dependiente del plan (max_watchers)