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/watch est 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é par max_watchers sur votre plan, et un plan où la surveillance est désactivée ne peut pas en créer du tout — les deux refus reviennent en 402. 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 :

  1. Création. Vous faites un POST d'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 ID wat_ est généré, et le moniteur est stocké active avec next_check_at défini à maintenant.
  2. Réclamation. Un watch_worker local au droplet scanne toutes les 60 secondes à la recherche de lignes actives dont le next_check_at est dépassé. Il les réclame sous FOR UPDATE SKIP LOCKED et 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.
  3. 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.
  4. 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.
  5. Notification. Lorsque le diff signale un changement, le webhook signé HMAC (si défini) et l'e-mail du propriétaire (si notify_email est activé) se déclenchent en parallèle, au mieux.
  6. 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 6043200 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 before et after à l'intérieur de changes sont 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_at est défini.
  • paused — hors du calendrier (next_check_at est null). Atteint soit par PATCH {"status": "paused"}, soit automatiquement après trois échecs de rendu consécutifs.
  • deleted — pierre tombale terminale, atteinte uniquement via DELETE. La ligne est conservée (donc l'historique des vérifications survit) mais elle n'apparaît jamais dans les listes et se lit comme 404 sur 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 moniteurs active qu'un projet peut détenir simultanément. Les moniteurs suspendus et supprimés ne comptent pas. Une valeur de 0 avec 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)