Surveillance
POST /v2/watch transforme n'importe quelle URL en moniteur de changements. Vous
enregistrez une page une fois ; un planificateur local au droplet la rend à
nouveau selon une cadence fixe dans un véritable Chrome headless, compare chaque
capture à la précédente, et vous notifie — par webhook, par e-mail, ou les deux —
lorsque la page change réellement. Cela remplace la plomberie de tâche cron, de
comparaison de différences et d'alerte que vous devriez sinon mettre en place
autour du point d'accès perceive : un seul
enregistrement au lieu d'un planificateur, d'un bucket de stockage et d'un
script de comparaison.
Voici le plus petit appel utile. Envoyez une URL et récupérez un moniteur programmé pour vérifier toutes les heures :
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 réponse est l'enregistrement complet du moniteur. Il est active
immédiatement, et next_check_at est défini au prochain tick 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
}
À signaler d'emblée.
/v2/watchest un point d'accès V2 avec son propre contrôle de plan, distinct des conversions V1. Le nombre de moniteurs actifs que vous pouvez détenir simultanément est plafonné parmax_watcherssur votre plan, et un plan où la surveillance est désactivée ne peut pas en créer du tout — les deux refus reviennent en402. Consultez la page tarifaire pour les allocations actuelles par palier.
Points d'accès
| Méthode | Chemin | Objectif |
|---|---|---|
POST |
/v2/watch |
Créer un moniteur pour une URL. Renvoie 201. |
GET |
/v2/watch |
Lister les moniteurs de ce projet, du plus récent au plus ancien. |
GET |
/v2/watch/{watcher_id} |
Récupérer l'enregistrement complet d'un moniteur. |
GET |
/v2/watch/{watcher_id}/snapshots |
Lister l'historique des vérifications d'un moniteur, du plus récent au plus ancien. |
PATCH |
/v2/watch/{watcher_id} |
Mettre à jour la cadence, les paramètres de diff, ou suspendre/reprendre. |
DELETE |
/v2/watch/{watcher_id} |
Suppression logique d'un moniteur (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
de serveur à serveur. C'est la voie qu'utilise chaque exemple ci-dessous.
X-API-Key: sk_live_your_private_key
Les clés publiques avec un jeton bearer JWT fonctionnent également, selon le même
flux que tous les autres points d'accès — générez un jeton avec votre clé pk_,
puis envoyez-le comme Authorization: Bearer <token>. Le flux complet, y compris
le verrouillage de domaine et le rafraîchissement du jeton, se trouve dans
le guide d'authentification.
Chaque clé API porte une liste blanche de points d'accès autorisés. Si
/v2/watch n'est pas sur 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 moniteurs, elle peut aussi
atteindre les routes par moniteur qu'elle possède — GET, PATCH et DELETE
sur /v2/watch/{watcher_id} et sa page /snapshots sont autorisés
automatiquement, donc vous n'avez pas à mettre chaque verbe sur la liste blanche
séparément.
Comment fonctionne la surveillance
Il n'y a aucune file d'attente externe derrière cela — pas de Google Cloud Tasks,
pas de planificateur tiers. Le calendrier vit entièrement dans la colonne de base
de données next_check_at, et un poller en cours de processus le pilote :
- Création. Vous faites un
POSTd'une URL. L'URL est filtrée contre la SSRF (un hôte privé, loopback ou metadata est rejeté avant qu'aucune ligne ne soit écrite), un IDwat_est généré, et le moniteur est stockéactiveavecnext_check_atdéfini à maintenant. - Réclamation. Un
watch_workerlocal au droplet scanne toutes les 60 secondes à la recherche de lignes actives dont lenext_check_atest dépassé. Il les réclame sousFOR UPDATE SKIP LOCKEDet avance le calendrier de chacune d'un intervalle complet dans la même transaction — ainsi un rendu lent n'est jamais réclamé deux fois, et un crash en plein rendu saute simplement un cycle. - Rendu. Chaque moniteur réclamé est rendu une fois via le singleton Chrome headless partagé — le même pipeline de capture qui sous-tend le point d'accès perceive. Le rendu est sans identifiants : aucune auth, aucun cookie, aucun en-tête n'est stocké, donc rien de secret ne reste au repos pour la vérification récurrente.
- Notation et diff. Un rendu dont le score est en dessous du seuil de qualité (0.4) ou signalé comme bloqué est enregistré comme une vérification à des fins d'audit uniquement — pas de hash de contenu, donc il ne devient jamais une référence de diff 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. Lorsque le diff signale un changement, le webhook signé HMAC
(si défini) et l'e-mail du propriétaire (si
notify_emailest activé) se déclenchent en parallèle, au mieux. - Reprogrammation. Le worker écrit le prochain
next_check_at. Trois échecs de rendu consécutifs suspendent le moniteur et envoient un e-mail au propriétaire au lieu de reprogrammer.
Parce que le calendrier est une colonne de base de données, l'indisponibilité ne nécessite aucune logique de récupération : le premier tick après le démarrage ramasse tout ce qui est en retard.
Paramètres de la requête
Création (POST /v2/watch)
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
url |
string |
— | La page à surveiller. Doit commencer par http:// ou https://. Max 2 048 caractères. Requis. |
frequency_minutes |
integer |
60 |
Minutes entre les vérifications. Plancher horaire strict : minimum 60, maximum 43200 (30 jours). |
diff_mode |
string |
"auto" |
Quelle 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 la SSRF immédiatement avant chaque livraison. Max 2 048 caractères ; doit être http(s). |
notify_email |
boolean |
true |
Envoyer un e-mail au propriétaire du projet lors d'un changement détecté et lors d'une suspension 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 — les moniteurs restent sans identifiants, la même posture que
le point d'accès 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 60–43200 qu'à la création. |
diff_mode |
string |
Changer la stratégie de diff. |
track_fields |
object |
Remplacer le sous-ensemble de champs suivis. |
webhook_url |
string |
Définir un nouveau webhook. Une chaîne vide est le signal explicite « efface-le » et stocke NULL. |
notify_email |
boolean |
Basculer l'e-mail du propriétaire. |
status |
string |
active ou paused. La reprise réarme le calendrier (next_check_at est défini au prochain tick) ; la suspension l'efface pour que le poller cesse de réclamer la ligne. |
Un corps vide ({}) est rejeté avec 422 plutôt que silencieusement ignoré.
Notez que status n'accepte ici que active ou paused — l'état terminal
deleted s'atteint via DELETE, jamais PATCH. Reprendre un moniteur suspendu
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 choisit laquelle des quatre stratégies tenant compte du type de
contenu le moteur exécute. auto exécute les quatre et fusionne leurs
constatations ; les modes nommés restreignent le diff à une seule stratégie.
diff_mode |
Stratégie | Ce qu'il signale |
|---|---|---|
auto (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é lorsque la similarité descend sous 0.98, pour qu'un mot réordonné ou une modification d'espace ne fasse pas osciller le moniteur. Porte un diff unifié plafonné à 100 lignes. |
structured |
Correspondance de listes par clé | Éléments ajoutés / supprimés / modifiés par champ à travers 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 | Tableaux mis en correspondance par légende/titre ; signale les changements de nombre de lignes, les tableaux ajoutés/supprimés, et les modifications de contenu à nombre de lignes égal (bornées à 100 lignes de contexte). |
metadata |
Comparaison de dict clé par clé | Champs de métadonnées de page ajoutés, supprimés et modifiés. |
Quel que soit le mode que vous choisissez, le similarity du snapshot est
toujours le ratio global de la capture entière (0.0–1.0). Sous un mode restreint,
cela signifie que similarity peut afficher une valeur basse tandis que
has_changes est false — une section que vous ne comparez pas a dérivé, mais
rien dans la stratégie que vous avez choisie n'a changé.
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 — ses 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 moniteur.
| Champ | Type | Description |
|---|---|---|
watcher_id |
string |
ID opaque (wat_...). Utilisez-le sur les routes par moniteur. |
url |
string |
L'URL surveillée. |
status |
string |
active, paused, ou deleted. |
frequency_minutes |
integer |
Cadence actuelle, après le 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 |
Si l'e-mail du propriétaire est activé. |
consecutive_errors |
integer |
Échecs de rendu d'affilée. Remis à 0 lors d'une vérification réussie ; à 3 le moniteur se suspend automatiquement. |
checks_count |
integer |
Total des vérifications exécutées, réussies ou échouées. |
last_check_at |
string |
Horodatage UTC de la vérification la plus récente, ou null. |
next_check_at |
string |
Horodatage UTC de la prochaine vérification programmée. null lorsque suspendu ou supprimé. |
last_change_at |
string |
Horodatage UTC du changement détecté le plus récent, ou null. |
created_at |
string |
Quand le moniteur a été créé. |
updated_at |
string |
Dernière mutation, ou null s'il n'a jamais été mis à jour. |
GET /v2/watch renvoie un WatcherSummary compact par ligne (il abandonne
diff_mode, track_fields, webhook_url, notify_email et updated_at)
enveloppé dans une enveloppe de page :
| Champ | Type | Description |
|---|---|---|
watchers |
array |
La page de résumés, du plus récent au plus ancien. |
skip |
integer |
Le décalage que vous avez demandé. |
limit |
integer |
La taille de page en vigueur. |
has_more |
boolean |
true lorsqu'il existe d'autres moniteurs au-delà de cette page. |
Historique des snapshots
GET /v2/watch/{watcher_id}/snapshots renvoie la chronologie des vérifications du
moniteur, 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 |
Si cette vérification a détecté un changement. |
similarity |
number |
Ratio global de la capture entière, 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. |
À signaler. Les valeurs
beforeetafterà l'intérieur dechangessont du contenu de page brut (texte de lien, métadonnées, valeurs JSON-LD), pas une sortie assainie. Si vous les rendez en HTML — un tableau de bord, un e-mail — 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 moniteur passe par trois états :
active— sur le calendrier du poller.next_check_atest défini.paused— hors du calendrier (next_check_atestnull). Atteint soit parPATCH {"status": "paused"}, soit automatiquement après trois échecs de rendu consécutifs.deleted— pierre tombale terminale, atteinte uniquement viaDELETE. La ligne est conservée (donc l'historique des vérifications survit) mais elle n'apparaît jamais dans les listes et se lit comme404sur les routes par moniteur.
Le plancher horaire est appliqué à deux endroits : à la création/mise à jour, et
de nouveau par le planificateur 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.
Suspension automatique
Après trois échecs de rendu consécutifs, le moniteur est mis à paused, son
calendrier est effacé, et — si notify_email est activé — le propriétaire reçoit
un e-mail de moniteur suspendu. Une seule vérification réussie remet
consecutive_errors à 0. Pour redémarrer un moniteur suspendu, faites un
PATCH pour le ramener à active, ce qui réarme next_check_at pour le prochain
tick.
Suppression
DELETE /v2/watch/{watcher_id} est une suppression logique : elle bascule
status à deleted, efface next_check_at, et renvoie l'enregistrement mis en
pierre tombale avec un 200. Elle est idempotente — supprimer un moniteur déjà
supprimé renvoie le même enregistrement inchangé. Les moniteurs supprimés cessent
immédiatement de compter dans votre plafond max_watchers (seuls les moniteurs
active comptent).
Exemples de code
curl — créer avec les valeurs par défaut
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 — toutes les 6 heures, webhook + suivi de tableaux
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 — suspendre, puis reprendre
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);
Contrôle de plan
La surveillance est contrôlée de deux façons, toutes deux distinctes de votre quota de conversions V1 :
watch_enabled— un drapeau booléen de plan. Un plan sans ce drapeau ne peut pas créer de moniteur du tout (402).max_watchers— le plafond du nombre de moniteursactivequ'un projet peut détenir simultanément. Les moniteurs suspendus et supprimés ne comptent pas. Une valeur de0avec le drapeau activé signifie illimité (entreprise/admin).
Créer un moniteur ne consomme pas une conversion ni une opération V2 — la
surveillance est contrôlée par le nombre de moniteurs que vous détenez, pas par un
compteur par appel. Parce que chaque moniteur vérifie selon une cadence horaire ou
plus lente, le volume total de vérifications d'un projet est borné par
max_watchers et le plancher horaire ensemble. Les allocations actuelles de
max_watchers par plan se trouvent sur la page tarifaire.
Réponses d'erreur
| Statut | Condition |
|---|---|
400 Bad Request |
L'URL résout vers une adresse privée, loopback, link-local ou metadata (protection SSRF au moment de la création). |
401 Unauthorized |
Clé API / jeton JWT manquant ou invalide. |
402 Payment Required |
La surveillance n'est pas activée sur votre plan, ou votre nombre de moniteurs actifs a atteint max_watchers (également appliqué lors d'une reprise paused→active). |
403 Forbidden |
/v2/watch n'est pas dans les points d'accès autorisés de la clé API. |
404 Not Found |
watcher_id inconnu, un appartenant à un autre projet, ou un déjà supprimé logiquement. |
422 Unprocessable Entity |
frequency_minutes en dessous de 60 ou au-dessus de 43200, un corps PATCH vide ({}), un mauvais enum dans diff_mode ou status, une url/webhook_url qui n'est pas http(s), ou tout champ inconnu. |
500 Internal Server Error |
Le moniteur 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 d'URL | 2 048 caractères |
Longueur de webhook_url |
2 048 caractères |
frequency_minutes |
60–43 200 (1 heure à 30 jours) |
| Plancher horaire | 60 minutes, appliqué à la création, la mise à jour et la planification |
| Intervalle de poll | 60 secondes (une vérification se déclenche dans la minute de son heure programmée) |
| Erreurs consécutives avant suspension automatique | 3 |
| Plancher de qualité de rendu (pas de diff en dessous) | 0.4 |
| Seuil de similarité de 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é comparé | plafonné à 200 000 caractères |
| Taille de page liste / snapshot | défaut 20, max 100 |
| Moniteurs actifs par projet | dépendant du plan (max_watchers) |