---
seo_title: Watch (fase 2): rilevamento modifiche siti web | EnConvert
meta_desc: Beta privata, fase 2: monitora qualsiasi URL a cadenza oraria o più lenta, con un webhook o un'email quando il contenuto della pagina cambia davvero.
keywords: api rilevamento modifiche sito web, come monitorare cambiamenti di una pagina web, webhook per monitoraggio modifiche sito, api per notifiche di cambio prezzo, tracciare modifiche di una url con api, confronto automatico di pagine web api, notifica webhook quando una pagina cambia, monitoraggio prezzo prodotto api
---

# API di rilevamento delle modifiche ai siti web

<div class="alert alert-warning">
<strong>Beta privata.</strong> 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 <a href="/it/docs/coming-soon">In arrivo</a>, e ogni rilascio viene annunciato nel <a href="/it/changelog">changelog</a>.
</div>

`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](/it/docs/endpoints/perceive.md): 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:

```bash
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:

```json
{
    "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.

```http
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](/it/docs/authentication.md).

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](/it/docs/endpoints/perceive.md). 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](#diff-modes). |
| `track_fields` | `object` | `null` | Sottoinsieme opzionale di campi/selettori per restringere cosa conta come cambiamento. Vedi [Tracciare un sottoinsieme di campi](#tracking-a-subset-of-fields). |
| `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](/it/docs/concepts/v1-and-v2.md).

### 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-modes }

`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 {: #tracking-a-subset-of-fields }

`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

```bash
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

```bash
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

```bash
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

```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

```javascript
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](/it/docs/reference/errors.md).

---

## 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`.
