API di rilevamento delle modifiche ai siti web#

Beta privata. Watch è già chiamabile oggi con la tua normale chiave API su qualsiasi piano a pagamento, e i watcher non costano ops; il piano gratuito Founding non può crearli. Non è annunciato né disponibile in generale: le forme di richiesta e risposta possono cambiare senza preavviso e non c'è alcun impegno di stabilità o di supporto, quindi non costruirci ancora sopra nulla di critico. La roadmap è su In arrivo, e ogni rilascio viene annunciato nel changelog.

POST /v2/watch è un'API di rilevamento delle modifiche ai siti web: registri una pagina una sola volta, e uno scheduler locale al droplet la ri-renderizza a cadenza fissa in un vero Chrome headless, confronta ogni cattura con quella precedente, e ti avvisa (via webhook firmato HMAC, via email o in entrambi i modi) quando la pagina cambia davvero. Sostituirà il cron job, il diffing e l'infrastruttura di notifica che dovresti altrimenti allestire intorno a l'endpoint perceive: un solo record al posto di uno scheduler, un bucket di storage e uno script di confronto.

Ecco la chiamata utile più semplice. Invia un URL e ricevi indietro un watcher programmato per controllare ogni ora:

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

Endpoint#

Metodo Percorso Scopo
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 metti in pausa/riprendi.
DELETE /v2/watch/{watcher_id} Elimina in modo soft un watcher (idempotente).

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


Autenticazione#

Autenticati con una chiave privata nell'header X-API-Key per le chiamate server-to-server. Questo è il percorso usato in tutti gli esempi seguenti.

X-API-Key: sk_your_private_key

Funzionano anche le chiavi pubbliche con un token bearer JWT, seguendo 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, si trova 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 di sua proprietà: GET, PATCH e DELETE su /v2/watch/{watcher_id} e la sua pagina /snapshots sono consentiti automaticamente, quindi non devi aggiungere ogni verbo separatamente all'allowlist.


Come funziona watch#

Non c'è nessuna coda esterna dietro a questo: niente Google Cloud Tasks, nessuno scheduler di terze parti. La pianificazione vive interamente nella colonna del database next_check_at, e un poller in-process la guida:

  1. Creazione. Esegui una POST con un URL. L'URL è controllato contro SSRF (un host privato, loopback o di metadata viene rifiutato prima che venga scritta qualsiasi riga), viene generato un ID wat_, e il watcher viene salvato come active con next_check_at impostato a ora.
  2. Acquisizione. Un watch_worker locale al droplet scansiona ogni 60 secondi le righe attive il cui next_check_at è passato. Le acquisisce con FOR UPDATE SKIP LOCKED e avanza la pianificazione di ciascuna di un intervallo completo nella stessa transazione. Così un render lento non viene mai acquisito due volte, e un crash a metà render salta semplicemente un ciclo.
  3. Rendering. Ogni watcher acquisito viene renderizzato una volta tramite il singleton Chrome headless condiviso, la stessa pipeline di cattura dietro l'endpoint perceive. Il render è privo di credenziali: non vengono salvati auth, cookie o header, quindi nessun segreto rimane a riposo per il controllo ricorrente.
  4. Punteggio e diff. Un render con punteggio sotto la soglia minima di qualità (0.4) o segnalato come bloccato viene registrato come controllo solo-audit, senza content hash, quindi non diventa mai una baseline per il diff e non attiva mai una notifica. Un render valido viene trasformato in una cattura (testo del contenuto principale più struttura estratta), confrontato con l'ultima cattura valida, e il verdetto viene scritto in una riga di snapshot.
  5. Notifica. Quando il diff segnala un cambiamento, il webhook firmato HMAC (se impostato) e l'email del proprietario (se notify_email è attivo) partono contemporaneamente, in modalità best-effort.
  6. Riprogrammazione. Il worker scrive il prossimo next_check_at. Tre fallimenti di render consecutivi mettono in pausa il watcher e inviano un'email al proprietario invece di riprogrammare.

Poiché la pianificazione è una colonna del database, il downtime non richiede alcuna logica di recupero: il primo tick dopo il riavvio raccoglie tutto ciò che è in ritardo.


Parametri della richiesta#

Crea (POST /v2/watch)#

Parametro Tipo Predefinito Descrizione
url string nessuno La pagina da monitorare. Deve iniziare con http:// o https://. Massimo 2,048 caratteri. Obbligatorio.
frequency_minutes integer 60 Minuti tra un controllo e l'altro. Soglia minima oraria fissa: minimo 60, massimo 43200 (30 giorni).
diff_mode string "auto" Quale strategia di diff applicare. auto, text, structured, tables, o metadata. Vedi Modalità diff.
track_fields object null Sottoinsieme opzionale di campi/selettori per restringere cosa conta come cambiamento. Vedi Tracciare un sottoinsieme di campi.
webhook_url string null Target opzionale per la notifica di cambiamento. Firmato HMAC, controllato contro SSRF immediatamente prima di ogni invio. Massimo 2,048 caratteri; deve essere http(s).
notify_email boolean true Invia un'email al proprietario del progetto quando viene rilevato un cambiamento e in caso di pausa automatica.

Lo schema della richiesta è rigido (extra="forbid"): un campo sconosciuto viene rifiutato con 422. Non c'è deliberatamente alcuna superficie auth, cookies o headers qui, perché i watcher restano privi di credenziali, la stessa impostazione dell'endpoint ingest.

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

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

Parametro Tipo Descrizione
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 per "cancellarlo" e salva NULL.
notify_email boolean Attiva/disattiva l'email al proprietario.
status string active o paused. Riprendere riarma la pianificazione (next_check_at viene impostato al prossimo tick); mettere in pausa la azzera, così il poller smette di acquisire la riga.

Un body vuoto ({}) viene rifiutato con 422 piuttosto che ignorato silenziosamente. Nota che status accetta solo active o paused qui. Lo stato terminale deleted si raggiunge tramite DELETE, mai PATCH. Riprendere un watcher in pausa conta come l'aggiunta di un monitor attivo, quindi ricontrolla lo stesso limite max_watchers della creazione e può restituire 402.


Modalità diff#

diff_mode sceglie quale delle quattro strategie sensibili al tipo di contenuto esegue il motore. auto le esegue tutte e quattro e unisce i risultati; le modalità con nome limitano il diff a una singola strategia.

diff_mode Strategia Cosa segnala
auto (predefinito) Tutte e quattro qui sotto Ogni tipo di cambiamento 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 spaziatura non fa oscillare il watcher. Include un diff unificato limitato a 100 righe.
structured Corrispondenza di liste con chiave Elementi aggiunti / rimossi / modificati per campo tra link (confrontati per href) e blocchi JSON-LD (confrontati per @type + name). Insensibile all'ordine.
tables Corrispondenza per intestazione di contesto Le tabelle vengono confrontate per didascalia/intestazione; segnala variazioni nel numero di righe, tabelle aggiunte/rimosse e modifiche di contenuto a parità di numero di righe (limitato a 100 righe di contesto).
metadata Confronto dizionario chiave per chiave Campi di metadata della pagina aggiunti, rimossi e modificati.

Qualunque modalità tu scelga, il similarity dello snapshot è sempre il rapporto complessivo sull'intera cattura (0.0–1.0). Sotto una modalità limitata questo significa che similarity può risultare basso mentre has_changes è false, perché è cambiata una sezione che non stai diffando, mentre nulla nella strategia scelta è cambiato.

Tracciare un sottoinsieme di campi#

track_fields restringe il diff ai cambiamenti la cui sezione, campo o chiave corrisponde a un termine tracciato. Accetta un oggetto le cui chiavi, più eventuali valori di lista, diventano i termini tracciati. Quindi {"metadata": ["title"]} traccia sia la sezione metadata sia il campo title. La corrispondenza è per token intero, non per sottostringa: un termine price corrisponde a offers.price ma non a priceCurrency.


Risposta#

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

Campo Tipo Descrizione
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 attuale, 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 cambiamento, o null.
notify_email boolean Se l'email al proprietario è attiva.
consecutive_errors integer Fallimenti di render consecutivi. Azzerato a 0 con un controllo riuscito; a 3 il watcher va in pausa automatica.
checks_count integer Totale dei controlli 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 mentre è in pausa o eliminato.
last_change_at string Timestamp UTC del cambiamento rilevato più recente, o null.
created_at string Quando il watcher è stato creato.
updated_at string Ultima modifica, 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) racchiuso in un page envelope:

Campo Tipo Descrizione
watchers array La pagina di riepiloghi, 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.

Cronologia degli snapshot#

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

Campo Tipo Descrizione
checked_at string Timestamp UTC del controllo.
has_changes boolean Se questo controllo ha rilevato un cambiamento.
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 cambiamento strutturati.
changes array Il diff strutturato: un record per ogni cambiamento, ciascuno con section, kind (added/removed/modified), key, field, before, after.

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


Ciclo di vita e pianificazione#

Un watcher attraversa tre stati:

  • active: nella pianificazione del poller. next_check_at è impostato.
  • paused: fuori dalla pianificazione (next_check_at è null). Si raggiunge tramite PATCH {"status": "paused"} oppure automaticamente dopo tre fallimenti di render consecutivi.
  • deleted: tombstone terminale, raggiunto solo tramite DELETE. La riga viene mantenuta (così la cronologia dei controlli sopravvive) ma non compare mai negli elenchi e risulta 404 sulle route per-watcher.

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

Pausa automatica#

Dopo tre fallimenti di render consecutivi, il watcher viene impostato su paused, la sua pianificazione viene azzerata e il proprietario riceve un'email di watcher in pausa se notify_email è attivo. Un singolo controllo riuscito azzera consecutive_errors a 0. Per riavviare un watcher in pausa, riportalo con PATCH a active, il che riarma next_check_at per il prossimo tick.

Eliminazione#

DELETE /v2/watch/{watcher_id} è un soft-delete: cambia status in deleted, azzera next_check_at, e restituisce il record tombstoned con un 200. È idempotente, quindi eliminare un watcher già eliminato restituisce lo stesso record invariato. I watcher eliminati smettono immediatamente di contare rispetto al limite max_watchers (contano solo i watcher active).


Esempi di codice#

curl: creazione con valori predefiniti#

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: ogni 6 ore, webhook più tracciamento tabelle#

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: pausa, poi ripresa#

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

Risposte di errore#

Stato Condizione
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 o token JWT mancante o non valido.
402 Payment Required Watch non è abilitato sul tuo piano, oppure il numero di watcher attivi ha raggiunto max_watchers (applicato anche alla ripresa paused→active).
403 Forbidden /v2/watch non è tra gli endpoint consentiti della chiave API.
404 Not Found watcher_id sconosciuto, di proprietà di un altro progetto, o già eliminato in modo soft.
422 Unprocessable Entity frequency_minutes sotto 60 o sopra 43200, un body PATCH vuoto ({}), un enum non valido in diff_mode o status, un url/webhook_url che non è http(s), o qualsiasi campo sconosciuto.
500 Internal Server Error Il watcher non è stato creato. Riprova.

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


Limiti#

Limite Valore
Lunghezza URL 2,048 caratteri
Watcher attivi (max_watchers) 20 / 100 / 500 su Indie / Studio / Production; watch non è disponibile su Founding
Ops per controllo 0 (i watcher non consumano mai la quota mensile di ops)
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 pianificazione
Intervallo di poll 60 secondi (un controllo scatta entro un minuto dall'orario pianificato)
Errori consecutivi prima della pausa automatica 3
Soglia minima di qualità del render (nessun diff sotto questo valore) 0.4
Soglia di similarità per cambiamento del testo 0.98
Diff di testo unificato limitato a 100 righe
Record di cambiamento per diff limitato a 500
Valore stringa per cambiamento troncato a 2,000 caratteri
Corpo di testo catturato messo a diff limitato a 200,000 caratteri
Dimensione pagina per elenco / snapshot predefinito 20, massimo 100

Domande frequenti#

Come configuro i webhook per il monitoraggio delle modifiche a un sito?#

Passa webhook_url quando crei il watcher con POST /v2/watch, oppure aggiungilo in seguito tramite PATCH /v2/watch/{watcher_id}. Quando un controllo rileva un cambiamento, EnConvert invia una POST firmata HMAC a quell'URL; il target viene controllato contro SSRF immediatamente prima di ogni invio, e una stringa vuota su PATCH lo azzera.

Con quale frequenza l'API può controllare una pagina per i cambiamenti?#

frequency_minutes imposta la cadenza, da 60 (la soglia minima oraria fissa) a 43200 (30 giorni). Il poller scansiona ogni 60 secondi, quindi un controllo scatta entro un minuto dal suo orario pianificato.

Perché il mio watcher si è messo in pausa da solo?#

Tre fallimenti di render consecutivi mettono automaticamente in pausa un watcher, azzerano la sua pianificazione e inviano un'email al proprietario se notify_email è attivo. Riportalo con PATCH a {"status": "active"} per riarmare next_check_at; un singolo controllo riuscito azzera consecutive_errors a 0.

Posso monitorare solo una parte di una pagina, come un prezzo o una tabella?#

Sì. track_fields restringe il diff ai cambiamenti la cui sezione, campo o chiave corrisponde a un termine tracciato (corrispondenza per token intero, quindi price corrisponde a offers.price ma non a priceCurrency), e diff_mode può limitare il rilevamento a una singola strategia come tables, structured, text, o metadata.