API de détection de changements de site#
POST /v2/watch est une API de détection de changements de site : vous
enregistrez une page une fois, et un planificateur local au droplet la
re-génère à une cadence fixe dans un véritable Chrome headless, compare
chaque capture à la précédente, et vous notifie par webhook signé HMAC,
par email, ou par les deux, quand la page change réellement. Cela
remplacera le cron job, la logique de diff et l'alerting que vous
devriez sinon mettre en place autour de
l'endpoint perceive : un seul enregistrement au
lieu d'un planificateur, d'un bucket de stockage et d'un script de
comparaison.
Voici l'appel minimal utile. Envoyez une URL et récupérez un watcher programmé pour vérifier toutes les heures :
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 réponse est l'enregistrement complet du watcher. Il est active
immédiatement, et next_check_at est réglé sur le prochain passage du
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
}
Endpoints#
| Méthode | Chemin | Objectif |
|---|---|---|
POST |
/v2/watch |
Crée un watcher pour une URL. Renvoie 201. |
GET |
/v2/watch |
Liste les watchers de ce projet, du plus récent au plus ancien. |
GET |
/v2/watch/{watcher_id} |
Récupère l'enregistrement complet d'un watcher. |
GET |
/v2/watch/{watcher_id}/snapshots |
Liste l'historique des vérifications d'un watcher, du plus récent au plus ancien. |
PATCH |
/v2/watch/{watcher_id} |
Met à jour la cadence, les paramètres de diff, ou met en pause/reprend. |
DELETE |
/v2/watch/{watcher_id} |
Supprime un watcher en soft-delete (idempotent). |
Content-Type : application/json sur POST et PATCH.
Authentification#
Authentifiez-vous avec une clé privée dans l'en-tête X-API-Key pour les
appels serveur à serveur. C'est la méthode utilisée par tous les exemples
ci-dessous.
X-API-Key: sk_your_private_key
Les clés publiques avec un token bearer JWT fonctionnent aussi, en suivant
le même flux que pour tous les autres endpoints : générez un token avec
votre clé pk_, puis envoyez-le en tant que Authorization: Bearer
<token>. Le flux complet, incluant le verrouillage de domaine et le
renouvellement de token, se trouve dans
le guide d'authentification.
Chaque clé API porte une liste blanche d'endpoints autorisés. Si
/v2/watch ne figure pas dans la liste de la clé, la requête de création
est rejetée avec 403. Une fois qu'une clé peut créer des watchers, elle
peut aussi accéder aux routes par watcher qu'elle possède : GET, PATCH
et DELETE sur /v2/watch/{watcher_id} et sa page /snapshots sont
autorisées automatiquement, donc vous n'avez pas besoin d'ajouter chaque
verbe séparément à la liste blanche.
Comment fonctionne watch#
Il n'y a aucune file d'attente externe derrière tout ça : pas de Google
Cloud Tasks, pas de scheduler tiers. Le planning vit entièrement dans la
colonne de base de données next_check_at, et un poller in-process le
pilote :
- Création. Vous faites un
POSTd'une URL. L'URL est filtrée contre le SSRF (un hôte privé, loopback, ou de métadonnées est rejeté avant qu'aucune ligne ne soit écrite), un IDwat_est généré, et le watcher est stocké enactiveavecnext_check_atréglé sur maintenant. - Prise en charge. Un
watch_workerlocal au droplet scanne toutes les 60 secondes les lignes actives dontnext_check_atest dépassé. Il les prend en charge sousFOR UPDATE SKIP LOCKEDet avance le planning de chacune d'un intervalle complet dans la même transaction. Ainsi un rendu lent n'est jamais pris en charge deux fois, et un crash en cours de rendu se contente de sauter un cycle. - Rendu. Chaque watcher pris en charge est rendu une fois via le singleton Chrome headless partagé, le même pipeline de capture que celui derrière l'endpoint perceive. Le rendu est sans identifiants : aucune authentification, cookie, ou en-tête n'est stocké, donc rien de secret ne persiste au repos pour la vérification récurrente.
- Score et diff. Un rendu dont le score est en dessous du plancher de qualité (0.4) ou signalé comme bloqué est enregistré comme une vérification à titre d'audit uniquement, sans hash de contenu, donc il ne devient jamais une base de comparaison et ne déclenche jamais de notification. Un bon rendu est transformé en capture (texte du contenu principal plus structure extraite), comparé à la dernière bonne capture, et le verdict est écrit dans une ligne de snapshot.
- Notification. Quand le diff signale un changement, le webhook signé
HMAC (s'il est configuré) et l'email du propriétaire (si
notify_emailest activé) se déclenchent en parallèle, en best-effort. - Replanification. Le worker écrit le prochain
next_check_at. Trois échecs de rendu consécutifs mettent le watcher en pause et envoient un email au propriétaire au lieu de replanifier.
Comme le planning est une colonne de base de données, aucune logique de reprise n'est nécessaire après une interruption : le premier passage après le démarrage rattrape tout ce qui est en retard.
Paramètres de requête#
Création (POST /v2/watch)#
| Paramètre | Type | Valeur par défaut | Description |
|---|---|---|---|
url |
string |
aucun | La page à surveiller. Doit commencer par http:// ou https://. Max 2,048 caractères. Obligatoire. |
frequency_minutes |
integer |
60 |
Minutes entre les vérifications. Plancher horaire strict : minimum 60, maximum 43200 (30 jours). |
diff_mode |
string |
"auto" |
La stratégie de diff à appliquer. auto, text, structured, tables, ou metadata. Voir Modes de diff. |
track_fields |
object |
null |
Sous-ensemble optionnel de champs/sélecteurs pour restreindre ce qui compte comme un changement. Voir Suivre un sous-ensemble de champs. |
webhook_url |
string |
null |
Cible optionnelle de notification de changement. Signée HMAC, filtrée contre le SSRF immédiatement avant chaque envoi. Max 2,048 caractères ; doit être en http(s). |
notify_email |
boolean |
true |
Envoie un email au propriétaire du projet en cas de changement détecté et en cas de mise en pause automatique. |
Le schéma de la requête est strict (extra="forbid") : un champ inconnu
est rejeté avec 422. Il n'y a délibérément aucune surface auth,
cookies, ou headers ici, car les watchers restent sans identifiants,
la même posture que l'endpoint ingest.
Mise à jour (PATCH /v2/watch/{watcher_id})#
Chaque champ est optionnel ; seules les clés présentes dans le corps sont appliquées.
| Paramètre | Type | Description |
|---|---|---|
frequency_minutes |
integer |
Nouvelle cadence. Mêmes bornes qu'à la création : de 60 à 43200. |
diff_mode |
string |
Change la stratégie de diff. |
track_fields |
object |
Remplace le sous-ensemble de champs suivis. |
webhook_url |
string |
Définit un nouveau webhook. Une chaîne vide est le signal explicite pour l'effacer et stocke NULL. |
notify_email |
boolean |
Active/désactive l'email au propriétaire. |
status |
string |
active ou paused. Reprendre réarme le planning (next_check_at est réglé sur le prochain passage) ; mettre en pause l'efface pour que le poller arrête de prendre en charge la ligne. |
Un corps vide ({}) est rejeté avec 422 plutôt que d'être
silencieusement ignoré. Notez que status n'accepte que active ou
paused ici. L'état terminal deleted est atteint via DELETE, jamais
via PATCH. Reprendre un watcher en pause compte comme l'ajout d'un
moniteur actif, donc cela revérifie le même plafond max_watchers qu'à la
création et peut renvoyer 402.
Modes de diff#
diff_mode détermine laquelle des quatre stratégies sensibles au type de
contenu le moteur exécute. auto exécute les quatre et fusionne leurs
résultats ; les modes nommés restreignent le diff à une seule stratégie.
diff_mode |
Stratégie | Ce qu'il signale |
|---|---|---|
auto (par défaut) |
Les quatre ci-dessous | Tout type de changement en une seule passe. |
text |
Ratio SequenceMatcher du contenu principal |
Le texte du corps a changé, signalé quand la similarité descend sous 0.98, de sorte qu'un mot réordonné ou une modification d'espacement ne déclenche pas le watcher à tort. Comporte un diff unifié plafonné à 100 lignes. |
structured |
Correspondance de listes par clé | Éléments ajoutés / supprimés / modifiés champ par champ parmi les liens (mis en correspondance par href) et les blocs JSON-LD (mis en correspondance par @type + name). Insensible à l'ordre. |
tables |
Correspondance par titre de contexte | Tables mises en correspondance par légende/titre ; signale les changements de nombre de lignes, les tables ajoutées/supprimées, et les modifications de contenu à nombre de lignes égal (limité à 100 lignes de contexte). |
metadata |
Comparaison de dictionnaire clé par clé | Champs de métadonnées de page ajoutés, supprimés et modifiés. |
Quel que soit le mode choisi, le similarity du snapshot est toujours le
ratio global sur l'ensemble de la capture (de 0.0 à 1.0). Sous un mode
restreint, cela signifie que similarity peut afficher une valeur basse
alors que has_changes est false, parce qu'une section que vous ne
diffez pas a dérivé sans que rien ne change dans la stratégie choisie.
Suivre un sous-ensemble de champs#
track_fields restreint le diff aux changements dont la section, le
champ, ou la clé correspond à un terme suivi. Il accepte un objet dont les
clés, plus toutes les valeurs de liste, deviennent les termes suivis.
Ainsi {"metadata": ["title"]} suit à la fois la section metadata et le
champ title. La correspondance se fait par token entier, pas par
sous-chaîne : un terme price correspond à offers.price mais pas à
priceCurrency.
Réponse#
POST, GET /v2/watch/{watcher_id}, PATCH, et DELETE renvoient tous
le même objet watcher.
| Champ | Type | Description |
|---|---|---|
watcher_id |
string |
ID opaque (wat_...). Utilisez-le sur les routes par watcher. |
url |
string |
L'URL surveillée. |
status |
string |
active, paused, ou deleted. |
frequency_minutes |
integer |
Cadence actuelle, après application du plancher horaire. |
diff_mode |
string |
La stratégie de diff active. |
track_fields |
object |
Le sous-ensemble de champs suivis, ou null. |
webhook_url |
string |
Le webhook de changement, ou null. |
notify_email |
boolean |
Indique si l'email au propriétaire est activé. |
consecutive_errors |
integer |
Nombre d'échecs de rendu consécutifs. Remis à 0 lors d'une vérification réussie ; à 3, le watcher se met en pause automatiquement. |
checks_count |
integer |
Nombre total de vérifications effectuées, réussies ou échouées. |
last_check_at |
string |
Horodatage UTC de la dernière vérification, ou null. |
next_check_at |
string |
Horodatage UTC de la prochaine vérification programmée. null tant que le watcher est en pause ou supprimé. |
last_change_at |
string |
Horodatage UTC du dernier changement détecté, ou null. |
created_at |
string |
Date de création du watcher. |
updated_at |
string |
Dernière modification, ou null si jamais mis à jour. |
GET /v2/watch renvoie un WatcherSummary compact par ligne (il omet
diff_mode, track_fields, webhook_url, notify_email, et
updated_at) enveloppé dans une pagination :
| Champ | Type | Description |
|---|---|---|
watchers |
array |
La page de résumés, du plus récent au plus ancien. |
skip |
integer |
Le décalage (offset) demandé. |
limit |
integer |
La taille de page en vigueur. |
has_more |
boolean |
true s'il existe d'autres watchers au-delà de cette page. |
Historique des snapshots#
GET /v2/watch/{watcher_id}/snapshots renvoie la chronologie des
vérifications du watcher, du plus récent au plus ancien. Chaque entrée
porte le verdict du diff. Le corps de la capture du snapshot lui-même vit
dans le stockage et n'est pas renvoyé ici.
| Champ | Type | Description |
|---|---|---|
checked_at |
string |
Horodatage UTC de la vérification. |
has_changes |
boolean |
Indique si cette vérification a détecté un changement. |
similarity |
number |
Ratio global sur l'ensemble de la capture, de 0.0 à 1.0, ou null. |
render_quality |
number |
Score de qualité de rendu pour la vérification, ou null. |
change_count |
integer |
Nombre d'enregistrements de changement structurés. |
changes |
array |
Le diff structuré : un enregistrement par changement, chacun avec section, kind (added/removed/modified), key, field, before, after. |
À souligner. Les valeurs
beforeetafterà l'intérieur dechangessont du contenu brut de la page (texte de lien, métadonnées, valeurs JSON-LD), pas une sortie assainie. Si vous les affichez en HTML (un dashboard, un email), vous devez les échapper vous-même. Les valeurs de chaîne longues sont déjà tronquées à 2,000 caractères, et un seul diff est plafonné à 500 enregistrements de changement.
Cycle de vie et planification#
Un watcher traverse trois états :
active: sur le planning du poller.next_check_atest défini.paused: hors planning (next_check_atvautnull). Atteint soit viaPATCH {"status": "paused"}, soit automatiquement après trois échecs de rendu consécutifs.deleted: la tombstone terminale, atteinte uniquement viaDELETE. La ligne est conservée (pour que l'historique des vérifications survive) mais elle n'apparaît jamais dans les listes et renvoie404sur les routes par watcher.
Le plancher horaire est appliqué à deux endroits : à la création/mise à
jour, et de nouveau par le scheduler lorsqu'il avance next_check_at.
Ainsi, même une ligne dont la cadence a été modifiée directement dans la
base de données ne peut jamais dépasser le plancher.
Mise en pause automatique#
Après trois échecs de rendu consécutifs, le watcher passe à paused,
son planning est effacé, et le propriétaire reçoit un email de mise en
pause si notify_email est activé. Une seule vérification
réussie remet consecutive_errors à 0. Pour relancer un watcher en
pause, remettez-le à active via PATCH, ce qui réarme next_check_at
pour le prochain passage.
Suppression#
DELETE /v2/watch/{watcher_id} est un soft-delete : il fait passer
status à deleted, efface next_check_at, et renvoie l'enregistrement
tombstone avec un 200. C'est idempotent, donc supprimer un watcher déjà
supprimé renvoie le même enregistrement inchangé. Les watchers supprimés
cessent immédiatement de compter dans votre plafond max_watchers (seuls
les watchers active comptent).
Exemples de code#
curl : création avec les valeurs par défaut#
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 : toutes les 6 heures, webhook plus suivi de table#
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 : pause, puis reprise#
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);
Réponses d'erreur#
| Statut | Condition |
|---|---|
400 Bad Request |
L'URL se résout vers une adresse privée, loopback, link-local, ou de métadonnées (protection SSRF au moment de la création). |
401 Unauthorized |
Clé API / token JWT manquant ou invalide. |
402 Payment Required |
Watch n'est pas activé sur votre plan, ou votre nombre de watchers actifs a atteint max_watchers (également appliqué lors d'une reprise paused→active). |
403 Forbidden |
/v2/watch ne figure pas dans les endpoints autorisés de la clé API. |
404 Not Found |
watcher_id inconnu, appartenant à un autre projet, ou déjà soft-supprimé. |
422 Unprocessable Entity |
frequency_minutes en dessous de 60 ou au-dessus de 43200, un corps PATCH vide ({}), une valeur d'énumération invalide dans diff_mode ou status, une url/webhook_url qui n'est pas en http(s), ou tout champ inconnu. |
500 Internal Server Error |
Le watcher n'a pas pu être créé. Réessayez. |
La référence complète des codes de statut se trouve dans le guide des codes d'erreur.
Limites#
| Limite | Valeur |
|---|---|
| Longueur de l'URL | 2,048 caractères |
Watchers actifs (max_watchers) |
20 / 100 / 500 sur Indie / Studio / Production ; watch n'est pas disponible sur Founding |
| Ops par vérification | 0 (les watchers ne puisent jamais dans le quota mensuel d'ops) |
Longueur de webhook_url |
2,048 caractères |
frequency_minutes |
de 60 à 43,200 (1 heure à 30 jours) |
| Plancher horaire | 60 minutes, appliqué à la création, à la mise à jour, et lors de la planification |
| Intervalle de scrutation | 60 secondes (une vérification se déclenche dans la minute suivant son heure programmée) |
| Erreurs consécutives avant mise en pause automatique | 3 |
| Plancher de qualité de rendu (aucun diff en dessous) | 0.4 |
| Seuil de similarité pour changement de texte | 0.98 |
| Diff de texte unifié | plafonné à 100 lignes |
| Enregistrements de changement par diff | plafonné à 500 |
| Valeur de chaîne par changement | tronquée à 2,000 caractères |
| Corps de texte capturé diffé | plafonné à 200,000 caractères |
| Taille de page liste / snapshot | 20 par défaut, max 100 |
Questions fréquentes#
Comment configurer des webhooks pour surveiller les changements d'un site ?#
Passez webhook_url lors de la création du watcher avec POST /v2/watch, ou ajoutez-le plus tard via PATCH /v2/watch/{watcher_id}. Quand une vérification détecte un changement, EnConvert envoie un POST signé HMAC vers cette URL ; la cible est filtrée contre le SSRF immédiatement avant chaque envoi, et une chaîne vide sur PATCH l'efface.
À quelle fréquence l'API peut-elle vérifier les changements d'une page ?#
frequency_minutes définit la cadence, de 60 (le plancher horaire strict) à 43200 (30 jours). Le poller scanne toutes les 60 secondes, donc une vérification se déclenche dans la minute suivant son heure programmée.
Pourquoi mon watcher s'est-il mis en pause tout seul ?#
Trois échecs de rendu consécutifs mettent automatiquement un watcher en pause, effacent son planning, et envoient un email au propriétaire si notify_email est activé. Remettez-le via PATCH avec {"status": "active"} pour réarmer next_check_at ; une seule vérification réussie remet consecutive_errors à 0.
Puis-je surveiller seulement une partie d'une page, comme un prix ou une table ?#
Oui. track_fields restreint le diff aux changements dont la section, le champ, ou la clé correspond à un terme suivi (correspondance par token entier, donc price correspond à offers.price mais pas à priceCurrency), et diff_mode peut restreindre la détection à une seule stratégie telle que tables, structured, text, ou metadata.