API di rilevamento delle modifiche ai siti web#
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:
- Creazione. Esegui una
POSTcon 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 IDwat_, e il watcher viene salvato comeactiveconnext_check_atimpostato a ora. - Acquisizione. Un
watch_workerlocale al droplet scansiona ogni 60 secondi le righe attive il cuinext_check_atè passato. Le acquisisce conFOR UPDATE SKIP LOCKEDe 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. - 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.
- 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.
- 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. - 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 60–43200 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
beforeeafterdentrochangessono 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 tramitePATCH {"status": "paused"}oppure automaticamente dopo tre fallimenti di render consecutivi.deleted: tombstone terminale, raggiunto solo tramiteDELETE. La riga viene mantenuta (così la cronologia dei controlli sopravvive) ma non compare mai negli elenchi e risulta404sulle 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.