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 damax_watcherssul tuo piano, e un piano con watch disabilitato non può crearne nessuno — entrambe le negazioni tornano come402. 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:
- Create. Fai
POSTdi 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 IDwat_viene coniato, e il watcher viene memorizzato comeactiveconnext_check_atimpostato su adesso. - Claim. Un
watch_workerlocale al droplet effettua una scansione ogni 60 secondi per le righe attive il cuinext_check_atè passato. Le rivendica sottoFOR UPDATE SKIP LOCKEDe 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. - 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.
- 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.
- 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. - 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 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 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
beforeeafterall'interno dichangessono 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 tramitePATCH {"status": "paused"}o automaticamente dopo tre render falliti consecutivi.deleted— tombstone terminale, raggiunto solo tramiteDELETE. La riga viene conservata (così la cronologia dei controlli sopravvive) ma non compare mai negli elenchi e si legge come404sulle 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 watcheractiveun progetto può mantenere contemporaneamente. I watcher in pausa ed eliminati non contano. Un valore di0con 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) |