Watch

POST /v2/watch trasforma qualsiasi URL in un monitor delle modifiche. Registri una pagina una volta sola; uno scheduler locale al droplet la ri-renderizza a cadenza fissa in Chrome headless reale, confronta ogni cattura con quella precedente e ti avvisa — via webhook, email o entrambi — quando la pagina cambia davvero. Sostituisce l'impalcatura di cron job più diffing più alerting che altrimenti dovresti costruire attorno all'endpoint perceive: un record invece di uno scheduler, un bucket di storage e uno script di confronto.

Ecco la chiamata utile più piccola. Invii un URL e ottieni un watcher pianificato per controllare ogni ora:

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 risposta è il record completo del watcher. È active immediatamente, e next_check_at è impostato sul prossimo 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
}

Da segnalare subito. /v2/watch è un endpoint V2 con un proprio gate di piano, separato dalle conversioni V1. Quanti watcher attivi puoi mantenere contemporaneamente è limitato da max_watchers sul tuo piano, e un piano con watch disabilitato non può crearne nessuno — entrambe le negazioni tornano come 402. Vedi la pagina dei prezzi per le allocazioni correnti per tier.


Endpoints

Method Path Purpose
POST /v2/watch Crea un watcher per un URL. Restituisce 201.
GET /v2/watch Elenca i watcher di questo progetto, dal più recente.
GET /v2/watch/{watcher_id} Recupera il record completo di un watcher.
GET /v2/watch/{watcher_id}/snapshots Elenca la cronologia dei controlli di un watcher, dal più recente.
PATCH /v2/watch/{watcher_id} Aggiorna la cadenza, le impostazioni di diff, o mette in pausa/riprende.
DELETE /v2/watch/{watcher_id} Elimina in modo soft un watcher (idempotente).

Content-Type: application/json su POST e PATCH.


Authentication

Autenticati con una chiave privata nell'header X-API-Key per le chiamate server-to-server. Questo è il percorso usato da ogni esempio qui sotto.

X-API-Key: sk_live_your_private_key

Funzionano anche le chiavi pubbliche con un token bearer JWT, usando lo stesso flusso di ogni altro endpoint — genera un token con la tua chiave pk_, poi invialo come Authorization: Bearer <token>. Il flusso completo, incluso il domain locking e il refresh del token, è nella guida all'autenticazione.

Ogni chiave API porta con sé un'allowlist di endpoint consentiti. Se /v2/watch non è nella lista della chiave, la richiesta di creazione viene rifiutata con 403. Una volta che una chiave può creare watcher, può anche raggiungere le route per-watcher che possiede — GET, PATCH e DELETE su /v2/watch/{watcher_id} e la sua pagina /snapshots sono consentiti automaticamente, quindi non devi inserire ogni verbo separatamente nell'allowlist.


How watch works

Non c'è nessuna coda esterna dietro a questo — niente Google Cloud Tasks, nessuno scheduler di terze parti. Lo schedule risiede interamente nella colonna del database next_check_at, e un poller in-process lo guida:

  1. Create. Fai POST di un URL. L'URL viene sottoposto a screening SSRF (un host privato, loopback o di metadata viene rifiutato prima che venga scritta qualsiasi riga), un ID wat_ viene coniato, e il watcher viene memorizzato come active con next_check_at impostato su adesso.
  2. Claim. Un watch_worker locale al droplet effettua una scansione ogni 60 secondi per le righe attive il cui next_check_at è passato. Le rivendica sotto FOR UPDATE SKIP LOCKED e fa avanzare lo schedule di ciascuna di un intero intervallo nella stessa transazione — così un render lento non viene mai rivendicato due volte, e un crash a metà render salta semplicemente un ciclo.
  3. Render. Ogni watcher rivendicato viene renderizzato una volta attraverso il singleton condiviso di Chrome headless — la stessa pipeline di cattura dietro l'endpoint perceive. Il render è credential-free: nessuna auth, cookie o header viene memorizzato, quindi nulla di segreto resta a riposo per il controllo ricorrente.
  4. Score and diff. Un render con punteggio sotto la soglia minima di qualità (0.4) o contrassegnato come bloccato viene registrato come un controllo di solo audit — nessun content hash, quindi non diventa mai una baseline di diff e non scatena mai una notifica. Un buon render viene trasformato in una cattura (testo del contenuto principale più struttura estratta), confrontato con l'ultima cattura buona, e il verdetto viene scritto in una riga di snapshot.
  5. Notify. Quando il diff segnala una modifica, il webhook firmato con HMAC (se impostato) e l'email del proprietario (se notify_email è attivo) partono in modo concorrente, best-effort.
  6. Reschedule. Il worker scrive il prossimo next_check_at. Tre render falliti consecutivi mettono in pausa il watcher e inviano un'email al proprietario invece di rischedulare.

Poiché lo schedule è una colonna del database, il downtime non necessita di logica di recupero: il primo tick dopo il boot raccoglie tutto ciò che è in ritardo.


Request parameters

Create (POST /v2/watch)

Parameter Type Default Description
url string La pagina da monitorare. Deve iniziare con http:// o https://. Massimo 2.048 caratteri. Obbligatorio.
frequency_minutes integer 60 Minuti tra i controlli. Soglia minima oraria rigida: minimo 60, massimo 43200 (30 giorni).
diff_mode string "auto" Quale strategia di diff applicare. auto, text, structured, tables o metadata. Vedi Diff modes.
track_fields object null Sottoinsieme opzionale di campo/selettore per restringere ciò che conta come modifica. Vedi Tracking a subset of fields.
webhook_url string null Destinazione opzionale per le notifiche di modifica. Firmata con HMAC, sottoposta a screening SSRF immediatamente prima di ogni consegna. Massimo 2.048 caratteri; deve essere http(s).
notify_email boolean true Invia un'email al proprietario del progetto su una modifica rilevata e su auto-pausa.

Lo schema della richiesta è rigido (extra="forbid"): un campo sconosciuto viene rifiutato con 422. Deliberatamente non c'è qui alcuna superficie auth, cookies o headers — i watcher restano credential-free, la stessa postura di l'endpoint ingest.

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

Ogni campo è opzionale; vengono applicate solo le chiavi presenti nel body.

Parameter Type Description
frequency_minutes integer Nuova cadenza. Stessi limiti 6043200 della creazione.
diff_mode string Cambia la strategia di diff.
track_fields object Sostituisce il sottoinsieme di campi tracciati.
webhook_url string Imposta un nuovo webhook. Una stringa vuota è il segnale esplicito di "rimuovilo" e memorizza NULL.
notify_email boolean Attiva/disattiva l'email del proprietario.
status string active o paused. La ripresa riarma lo schedule (next_check_at viene impostato sul prossimo tick); la pausa lo azzera così il poller smette di rivendicare la riga.

Un body vuoto ({}) viene rifiutato con 422 invece di essere silenziosamente trattato come no-op. Nota che status qui accetta solo active o paused — lo stato terminale deleted si raggiunge tramite DELETE, mai con PATCH. Riprendere un watcher in pausa conta come aggiungere un monitor attivo, quindi ricontrolla lo stesso cap max_watchers della creazione e può restituire 402.


Diff modes

diff_mode seleziona quale delle quattro strategie content-type-aware esegue il motore. auto esegue tutte e quattro e unisce i loro risultati; le modalità nominate restringono il diff a una singola strategia.

diff_mode Strategy What it flags
auto (default) Tutte e quattro qui sotto Ogni tipo di modifica in un solo passaggio.
text Rapporto SequenceMatcher del contenuto principale Il testo del corpo è cambiato — segnalato quando la similarità scende sotto 0.98, così una parola riordinata o una modifica di spazi non fa oscillare il watcher. Porta con sé un unified diff limitato a 100 righe.
structured Matching di liste con chiave Elementi aggiunti / rimossi / modificati per campo attraverso i link (abbinati per href) e i blocchi JSON-LD (abbinati per @type + name). Insensibile all'ordine.
tables Matching per intestazione di contesto Tabelle abbinate per caption/intestazione; riporta variazioni del conteggio righe, tabelle aggiunte/rimosse e modifiche di contenuto a parità di conteggio righe (limitate a 100 righe di contesto).
metadata Confronto dict chiave per chiave Campi di metadata di pagina aggiunti, rimossi e modificati.

Qualunque modalità tu scelga, la similarity dello snapshot è sempre il rapporto complessivo sull'intera cattura (0.0–1.0). In una modalità ristretta questo significa che similarity può risultare bassa mentre has_changes è false — una sezione che non stai confrontando è cambiata, ma nulla nella strategia scelta è cambiato.

Tracking a subset of fields

track_fields restringe il diff alle modifiche la cui sezione, campo o chiave corrisponde a un termine tracciato. Accetta un oggetto — le sue chiavi, più qualsiasi valore di lista, diventano i termini tracciati. Quindi {"metadata": ["title"]} traccia sia la sezione metadata sia il campo title. Il matching è su token interi, non su sottostringa: un termine price corrisponde a offers.price ma non a priceCurrency.


Response

POST, GET /v2/watch/{watcher_id}, PATCH e DELETE restituiscono tutti lo stesso oggetto watcher.

Field Type Description
watcher_id string ID opaco (wat_...). Usalo sulle route per-watcher.
url string L'URL monitorato.
status string active, paused o deleted.
frequency_minutes integer Cadenza corrente, dopo la soglia minima oraria.
diff_mode string La strategia di diff attiva.
track_fields object Il sottoinsieme di campi tracciati, o null.
webhook_url string Il webhook di modifica, o null.
notify_email boolean Se l'email del proprietario è attiva.
consecutive_errors integer Render falliti di fila. Riportato a 0 su un controllo riuscito; a 3 il watcher si mette in pausa automaticamente.
checks_count integer Controlli totali eseguiti, riusciti o falliti.
last_check_at string Timestamp UTC del controllo più recente, o null.
next_check_at string Timestamp UTC del prossimo controllo pianificato. null quando in pausa o eliminato.
last_change_at string Timestamp UTC della modifica rilevata più recente, o null.
created_at string Quando il watcher è stato creato.
updated_at string Ultima mutazione, o null se mai aggiornato.

GET /v2/watch restituisce un WatcherSummary compatto per riga (omette diff_mode, track_fields, webhook_url, notify_email e updated_at) avvolto in una busta di paginazione:

Field Type Description
watchers array La pagina di summary, dal più recente.
skip integer L'offset richiesto.
limit integer La dimensione di pagina in vigore.
has_more boolean true quando esistono altri watcher oltre questa pagina.

Snapshot history

GET /v2/watch/{watcher_id}/snapshots restituisce la timeline dei controlli del watcher, dal più recente. Ogni voce porta con sé il verdetto del diff — il corpo della cattura dello snapshot stesso risiede nello storage e non viene restituito qui.

Field Type Description
checked_at string Timestamp UTC del controllo.
has_changes boolean Se questo controllo ha rilevato una modifica.
similarity number Rapporto complessivo sull'intera cattura, 0.0–1.0, o null.
render_quality number Punteggio di qualità del render per il controllo, o null.
change_count integer Numero di record di modifica strutturati.
changes array Il diff strutturato: un record per modifica, ciascuno con section, kind (added/removed/modified), key, field, before, after.

Da segnalare. I valori before e after all'interno di changes sono contenuto grezzo della pagina (testo dei link, metadata, valori JSON-LD), non output sanitizzato. Se li renderizzi in HTML — una dashboard, un'email — devi effettuare tu stesso l'escape. I valori di stringa lunghi sono già troncati a 2.000 caratteri, e un singolo diff è limitato a 500 record di modifica.


Lifecycle and scheduling

Un watcher attraversa tre stati:

  • active — sullo schedule del poller. next_check_at è impostato.
  • paused — fuori dallo schedule (next_check_at è null). Raggiunto o tramite PATCH {"status": "paused"} o automaticamente dopo tre render falliti consecutivi.
  • deleted — tombstone terminale, raggiunto solo tramite DELETE. La riga viene conservata (così la cronologia dei controlli sopravvive) ma non compare mai negli elenchi e si legge come 404 sulle route per-watcher.

La soglia minima oraria è applicata in due punti: alla creazione/aggiornamento, e di nuovo dallo scheduler quando fa avanzare next_check_at. Così anche una riga la cui cadenza è stata modificata direttamente nel database non può mai superare la soglia minima.

Auto-pause

Dopo tre render falliti consecutivi, il watcher viene impostato su paused, il suo schedule viene azzerato, e — se notify_email è attivo — il proprietario riceve un'email di watcher in pausa. Un singolo controllo riuscito riporta consecutive_errors a 0. Per riavviare un watcher in pausa, fai PATCH riportandolo ad active, il che riarma next_check_at per il prossimo tick.

Deletion

DELETE /v2/watch/{watcher_id} è un soft-delete: capovolge status a deleted, azzera next_check_at, e restituisce il record con tombstone con un 200. È idempotente — eliminare un watcher già eliminato restituisce lo stesso record invariato. I watcher eliminati smettono di contare contro il tuo cap max_watchers immediatamente (contano solo i watcher active).


Code examples

curl — create with defaults

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 — every-6-hours, webhook + table tracking

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 — pause, then resume

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);

Plan gating

Watch è gated in due modi, entrambi separati dalla tua quota di conversioni V1:

  • watch_enabled — un flag booleano di piano. Un piano senza di esso non può creare affatto un watcher (402).
  • max_watchers — il cap su quanti watcher active un progetto può mantenere contemporaneamente. I watcher in pausa ed eliminati non contano. Un valore di 0 con il flag attivo significa illimitato (enterprise/admin).

Creare un watcher non consuma una conversione o un'operazione V2 — watch è gated da quanti watcher mantieni, non da un contatore per chiamata. Poiché ogni watcher controlla a cadenza oraria-o-più-lenta, il volume totale di controlli di un progetto è limitato congiuntamente da max_watchers e dalla soglia minima oraria. Le allocazioni correnti di max_watchers per piano risiedono su la pagina dei prezzi.


Error responses

Status Condition
400 Bad Request L'URL si risolve in un indirizzo privato, loopback, link-local o di metadata (protezione SSRF al momento della creazione).
401 Unauthorized Chiave API / token JWT mancante o non valido.
402 Payment Required Watch non è abilitato sul tuo piano, o il conteggio dei watcher attivi ha raggiunto max_watchers (applicato anche su una ripresa paused→active).
403 Forbidden /v2/watch non è tra gli endpoint consentiti della chiave API.
404 Not Found watcher_id sconosciuto, uno posseduto da un altro progetto, o uno già soft-deleted.
422 Unprocessable Entity frequency_minutes sotto 60 o sopra 43200, un body PATCH vuoto ({}), un enum errato in diff_mode o status, un url/webhook_url che non è http(s), o qualsiasi campo sconosciuto.
500 Internal Server Error Il watcher non ha potuto essere creato. Riprova.

Il riferimento completo dei codici di stato è nella guida ai codici di errore.


Limits

Limit Value
Lunghezza URL 2.048 caratteri
Lunghezza webhook_url 2.048 caratteri
frequency_minutes 60–43.200 (da 1 ora a 30 giorni)
Soglia minima oraria 60 minuti, applicata alla creazione, all'aggiornamento e alla schedulazione
Intervallo di poll 60 secondi (un controllo parte entro un minuto dal suo orario pianificato)
Errori consecutivi prima dell'auto-pausa 3
Soglia minima di qualità del render (nessun diff sotto di essa) 0.4
Soglia di similarità per le modifiche di testo 0.98
Unified text diff limitato a 100 righe
Record di modifica per diff limitato a 500
Valore di stringa per modifica troncato a 2.000 caratteri
Corpo di testo catturato confrontato limitato a 200.000 caratteri
Dimensione pagina di lista / snapshot default 20, max 100
Watcher attivi per progetto dipendente dal piano (max_watchers)