Watch

POST /v2/watch verwandelt jede URL in einen Änderungsmonitor. Du registrierst eine Seite einmal; ein droplet-lokaler Scheduler rendert sie in einem festen Takt erneut in echtem Headless-Chrome, vergleicht jede Aufnahme mit der vorherigen und benachrichtigt dich — per Webhook, E-Mail oder beidem —, wenn sich die Seite tatsächlich ändert. Das ersetzt den Cron-Job plus Diffing plus Alerting-Verkabelung, die du sonst um den perceive-Endpunkt herum aufbauen müsstest: ein einziger Datensatz statt eines Schedulers, eines Storage-Buckets und eines Vergleichsskripts.

Hier ist der kleinste nützliche Aufruf. Sende eine URL und erhalte einen Watcher zurück, der so eingeplant ist, dass er stündlich prüft:

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"
  }'

Die Antwort ist der vollständige Watcher-Datensatz. Er ist sofort active, und next_check_at ist auf den nächsten Tick des Pollers gesetzt:

{
    "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
}

Vorab erwähnenswert. /v2/watch ist ein V2-Endpunkt mit eigenem Plan-Gate, getrennt von V1-Konvertierungen. Wie viele aktive Watcher du gleichzeitig halten kannst, ist durch max_watchers in deinem Plan begrenzt, und ein Plan, in dem Watch deaktiviert ist, kann gar keinen erstellen — beide Ablehnungen kommen als 402 zurück. Aktuelle Kontingente pro Stufe findest du auf der Preisseite.


Endpunkte

Methode Pfad Zweck
POST /v2/watch Erstellt einen Watcher für eine URL. Gibt 201 zurück.
GET /v2/watch Listet die Watcher dieses Projekts auf, neueste zuerst.
GET /v2/watch/{watcher_id} Ruft den vollständigen Datensatz eines Watchers ab.
GET /v2/watch/{watcher_id}/snapshots Listet den Prüfverlauf eines Watchers auf, neueste zuerst.
PATCH /v2/watch/{watcher_id} Aktualisiert Takt, Diff-Einstellungen oder pausiert/setzt fort.
DELETE /v2/watch/{watcher_id} Soft-Delete eines Watchers (idempotent).

Content-Type: application/json bei POST und PATCH.


Authentifizierung

Authentifiziere dich mit einem privaten Schlüssel im X-API-Key-Header für Server-zu-Server-Aufrufe. Diesen Weg nutzt jedes Beispiel weiter unten.

X-API-Key: sk_live_your_private_key

Öffentliche Schlüssel mit einem JWT-Bearer-Token funktionieren ebenfalls und nutzen denselben Ablauf wie jeder andere Endpunkt — erzeuge ein Token mit deinem pk_-Schlüssel und sende es dann als Authorization: Bearer <token>. Der vollständige Ablauf, einschließlich Domain-Locking und Token-Refresh, steht in der Authentifizierungs-Anleitung.

Jeder API-Schlüssel trägt eine Allowlist erlaubter Endpunkte. Wenn /v2/watch nicht auf der Liste des Schlüssels steht, wird die Erstellungsanfrage mit 403 abgelehnt. Sobald ein Schlüssel Watcher erstellen darf, kann er auch die ihm gehörenden Routen pro Watcher erreichen — GET, PATCH und DELETE auf /v2/watch/{watcher_id} sowie dessen /snapshots-Seite werden automatisch durchgelassen, sodass du nicht jedes Verb einzeln auf die Allowlist setzen musst.


So funktioniert Watch

Dahinter steckt keine externe Queue — keine Google Cloud Tasks, kein Drittanbieter-Scheduler. Der Zeitplan lebt vollständig in der Datenbankspalte next_check_at, und ein In-Process-Poller treibt ihn an:

  1. Erstellen. Du sendest eine URL per POST. Die URL wird SSRF-geprüft (ein privater, Loopback- oder Metadaten-Host wird abgelehnt, bevor eine Zeile geschrieben wird), eine wat_-ID wird erzeugt, und der Watcher wird active mit auf jetzt gesetztem next_check_at gespeichert.
  2. Beanspruchen. Ein droplet-lokaler watch_worker scannt alle 60 Sekunden nach aktiven Zeilen, deren next_check_at verstrichen ist. Er beansprucht sie unter FOR UPDATE SKIP LOCKED und schiebt den Zeitplan jeder einzelnen in derselben Transaktion um ein volles Intervall vor — so wird ein langsames Rendern nie doppelt beansprucht, und ein Absturz mitten im Rendern überspringt nur einen Zyklus.
  3. Rendern. Jeder beanspruchte Watcher wird einmal über das gemeinsame Headless-Chrome-Singleton gerendert — dieselbe Capture-Pipeline hinter dem perceive-Endpunkt. Das Rendern ist credential-frei: Es werden keine Auth-Daten, Cookies oder Header gespeichert, sodass für die wiederkehrende Prüfung nichts Geheimes ruhend abgelegt ist.
  4. Bewerten und vergleichen. Ein Render-Vorgang, der unter dem Qualitäts-Mindestwert (0.4) liegt oder als blockiert markiert ist, wird als reine Audit-Prüfung erfasst — kein Content-Hash, sodass er nie zur Diff-Baseline wird und nie eine Benachrichtigung auslöst. Ein guter Render-Vorgang wird in eine Aufnahme umgewandelt (Haupttext plus extrahierte Struktur), gegen die letzte gute Aufnahme verglichen, und das Urteil wird in eine Snapshot-Zeile geschrieben.
  5. Benachrichtigen. Wenn der Diff eine Änderung meldet, feuern der HMAC-signierte Webhook (falls gesetzt) und die Eigentümer-E-Mail (falls notify_email aktiviert ist) gleichzeitig, nach Best-Effort.
  6. Neu einplanen. Der Worker schreibt das nächste next_check_at. Drei aufeinanderfolgende Render-Fehler pausieren den Watcher und schicken dem Eigentümer eine E-Mail, statt neu einzuplanen.

Weil der Zeitplan eine Datenbankspalte ist, braucht eine Ausfallzeit keine Wiederherstellungslogik: Der erste Tick nach dem Boot fegt alles Überfällige auf.


Anfrageparameter

Erstellen (POST /v2/watch)

Parameter Typ Standard Beschreibung
url string Die zu überwachende Seite. Muss mit http:// oder https:// beginnen. Maximal 2.048 Zeichen. Erforderlich.
frequency_minutes integer 60 Minuten zwischen den Prüfungen. Harter stündlicher Mindestwert: Minimum 60, Maximum 43200 (30 Tage).
diff_mode string "auto" Welche Diff-Strategie angewandt wird. auto, text, structured, tables oder metadata. Siehe Diff-Modi.
track_fields object null Optionale Feld-/Selektor-Teilmenge, um einzugrenzen, was als Änderung zählt. Siehe Eine Teilmenge von Feldern verfolgen.
webhook_url string null Optionales Ziel für Änderungsbenachrichtigungen. HMAC-signiert, unmittelbar vor jeder Zustellung SSRF-geprüft. Maximal 2.048 Zeichen; muss http(s) sein.
notify_email boolean true Schickt dem Projekteigentümer eine E-Mail bei einer erkannten Änderung und beim Auto-Pause.

Das Anfrageschema ist strikt (extra="forbid"): Ein unbekanntes Feld wird mit 422 abgelehnt. Es gibt hier bewusst keine auth-, cookies- oder headers-Oberfläche — Watcher bleiben credential-frei, dieselbe Haltung wie der ingest-Endpunkt.

Aktualisieren (PATCH /v2/watch/{watcher_id})

Jedes Feld ist optional; es werden nur die im Body vorhandenen Schlüssel angewendet.

Parameter Typ Beschreibung
frequency_minutes integer Neuer Takt. Dieselben 6043200-Grenzen wie beim Erstellen.
diff_mode string Wechselt die Diff-Strategie.
track_fields object Ersetzt die Teilmenge verfolgter Felder.
webhook_url string Setzt einen neuen Webhook. Ein leerer String ist das explizite "Löschen"-Signal und speichert NULL.
notify_email boolean Schaltet die Eigentümer-E-Mail um.
status string active oder paused. Das Fortsetzen scharft den Zeitplan erneut (next_check_at wird auf den nächsten Tick gesetzt); das Pausieren löscht ihn, sodass der Poller die Zeile nicht mehr beansprucht.

Ein leerer Body ({}) wird mit 422 abgelehnt, statt still als No-Op zu gelten. Beachte, dass status hier nur active oder paused akzeptiert — der terminale deleted-Zustand wird über DELETE erreicht, nie über PATCH. Das Fortsetzen eines pausierten Watchers zählt als Hinzufügen eines aktiven Monitors, prüft also dasselbe max_watchers-Limit wie das Erstellen erneut und kann 402 zurückgeben.


Diff-Modi

diff_mode wählt, welche von vier inhaltstyp-bewussten Strategien die Engine ausführt. auto führt alle vier aus und führt ihre Ergebnisse zusammen; die benannten Modi schränken den Diff auf eine einzelne Strategie ein.

diff_mode Strategie Was markiert wird
auto (Standard) Alle vier unten Jede Art von Änderung in einem Durchlauf.
text SequenceMatcher-Verhältnis des Haupttextes Body-Text geändert — markiert, wenn die Ähnlichkeit unter 0.98 fällt, sodass ein umsortiertes Wort oder eine Whitespace-Änderung den Watcher nicht flattern lässt. Trägt einen Unified-Diff, gedeckelt auf 100 Zeilen.
structured Abgleich keyed Listen Hinzugefügte/entfernte/pro Feld geänderte Elemente über Links (abgeglichen per href) und JSON-LD-Blöcke (abgeglichen per @type + name). Reihenfolge-unabhängig.
tables Abgleich per Kontext-Überschrift Tabellen abgeglichen per Beschriftung/Überschrift; meldet Änderungen der Zeilenzahl, hinzugefügte/entfernte Tabellen und Inhaltsänderungen bei gleicher Zeilenzahl (begrenzt auf 100 Zeilen Kontext).
metadata Schlüssel-für-Schlüssel-Dict-Vergleich Hinzugefügte, entfernte und geänderte Felder der Seitenmetadaten.

Welchen Modus du auch wählst, die similarity des Snapshots ist stets das Gesamtverhältnis der vollständigen Aufnahme (0.0–1.0). Unter einem eingeschränkten Modus bedeutet das, dass similarity niedrig sein kann, während has_changes false ist — ein Abschnitt, den du nicht vergleichst, ist abgedriftet, aber in deiner gewählten Strategie hat sich nichts geändert.

Eine Teilmenge von Feldern verfolgen

track_fields grenzt den Diff auf Änderungen ein, deren Abschnitt, Feld oder Schlüssel mit einem verfolgten Begriff übereinstimmt. Es akzeptiert ein Objekt — seine Schlüssel sowie alle Listenwerte werden zu den verfolgten Begriffen. So verfolgt {"metadata": ["title"]} sowohl den metadata-Abschnitt als auch das title-Feld. Der Abgleich erfolgt auf ganze Token, nicht auf Teilstrings: Ein Begriff price passt auf offers.price, aber nicht auf priceCurrency.


Antwort

POST, GET /v2/watch/{watcher_id}, PATCH und DELETE geben alle dasselbe Watcher-Objekt zurück.

Feld Typ Beschreibung
watcher_id string Opake ID (wat_...). Verwende sie auf den Routen pro Watcher.
url string Die überwachte URL.
status string active, paused oder deleted.
frequency_minutes integer Aktueller Takt, nach dem stündlichen Mindestwert.
diff_mode string Die aktive Diff-Strategie.
track_fields object Die Teilmenge verfolgter Felder oder null.
webhook_url string Der Änderungs-Webhook oder null.
notify_email boolean Ob die Eigentümer-E-Mail aktiviert ist.
consecutive_errors integer Render-Fehler in Folge. Wird bei einer erfolgreichen Prüfung auf 0 zurückgesetzt; bei 3 pausiert der Watcher automatisch.
checks_count integer Gesamtzahl der durchgeführten Prüfungen, erfolgreich oder fehlgeschlagen.
last_check_at string UTC-Zeitstempel der jüngsten Prüfung oder null.
next_check_at string UTC-Zeitstempel der nächsten geplanten Prüfung. null, solange pausiert oder gelöscht.
last_change_at string UTC-Zeitstempel der jüngsten erkannten Änderung oder null.
created_at string Wann der Watcher erstellt wurde.
updated_at string Letzte Mutation oder null, falls nie aktualisiert.

GET /v2/watch gibt pro Zeile eine kompakte WatcherSummary zurück (sie lässt diff_mode, track_fields, webhook_url, notify_email und updated_at weg), eingewickelt in einen Seiten-Umschlag:

Feld Typ Beschreibung
watchers array Die Seite der Zusammenfassungen, neueste zuerst.
skip integer Der von dir angeforderte Offset.
limit integer Die geltende Seitengröße.
has_more boolean true, wenn jenseits dieser Seite weitere Watcher existieren.

Snapshot-Verlauf

GET /v2/watch/{watcher_id}/snapshots gibt die Prüf-Timeline des Watchers zurück, neueste zuerst. Jeder Eintrag trägt das Diff-Urteil — der Snapshot-Capture-Body selbst liegt im Storage und wird hier nicht zurückgegeben.

Feld Typ Beschreibung
checked_at string UTC-Zeitstempel der Prüfung.
has_changes boolean Ob diese Prüfung eine Änderung erkannt hat.
similarity number Gesamtverhältnis der vollständigen Aufnahme, 0.0–1.0, oder null.
render_quality number Render-Qualitätswert für die Prüfung oder null.
change_count integer Anzahl der strukturierten Änderungsdatensätze.
changes array Der strukturierte Diff: ein Datensatz pro Änderung, jeweils mit section, kind (added/removed/modified), key, field, before, after.

Erwähnenswert. Die before- und after-Werte innerhalb von changes sind roher Seiteninhalt (Linktext, Metadaten, JSON-LD-Werte), keine bereinigte Ausgabe. Wenn du sie in HTML renderst — ein Dashboard, eine E-Mail —, musst du sie selbst escapen. Lange Stringwerte sind bereits auf 2.000 Zeichen gekürzt, und ein einzelner Diff ist auf 500 Änderungsdatensätze gedeckelt.


Lebenszyklus und Planung

Ein Watcher durchläuft drei Zustände:

  • active — auf dem Zeitplan des Pollers. next_check_at ist gesetzt.
  • paused — außerhalb des Zeitplans (next_check_at ist null). Erreicht entweder durch PATCH {"status": "paused"} oder automatisch nach drei aufeinanderfolgenden Render-Fehlern.
  • deleted — terminaler Grabstein, nur über DELETE erreichbar. Die Zeile bleibt erhalten (sodass der Prüfverlauf überlebt), erscheint aber nie in Listen und liest sich als 404 auf den Routen pro Watcher.

Der stündliche Mindestwert wird an zwei Stellen durchgesetzt: beim Erstellen/Aktualisieren und erneut durch den Scheduler, wenn er next_check_at vorschiebt. So kann selbst eine Zeile, deren Takt direkt in der Datenbank bearbeitet wurde, den Mindestwert nie überholen.

Auto-Pause

Nach drei aufeinanderfolgenden Render-Fehlern wird der Watcher auf paused gesetzt, sein Zeitplan wird gelöscht, und — falls notify_email aktiviert ist — erhält der Eigentümer eine E-Mail über den pausierten Watcher. Eine einzige erfolgreiche Prüfung setzt consecutive_errors auf 0 zurück. Um einen pausierten Watcher neu zu starten, setze ihn per PATCH zurück auf active, was next_check_at für den nächsten Tick neu scharft.

Löschung

DELETE /v2/watch/{watcher_id} ist ein Soft-Delete: Es setzt status auf deleted, löscht next_check_at und gibt den als Grabstein markierten Datensatz mit einem 200 zurück. Es ist idempotent — das Löschen eines bereits gelöschten Watchers gibt denselben Datensatz unverändert zurück. Gelöschte Watcher zählen sofort nicht mehr gegen dein max_watchers-Limit (nur active-Watcher zählen).


Code-Beispiele

curl — mit Standardwerten erstellen

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 — alle 6 Stunden, Webhook + Tabellen-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 — pausieren, dann fortsetzen

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 ist auf zwei Wegen gated, beide getrennt von deinem V1-Konvertierungskontingent:

  • watch_enabled — ein boolesches Plan-Flag. Ein Plan ohne dieses Flag kann gar keinen Watcher erstellen (402).
  • max_watchers — das Limit, wie viele active-Watcher ein Projekt gleichzeitig halten darf. Pausierte und gelöschte Watcher zählen nicht. Ein Wert von 0 bei aktiviertem Flag bedeutet unbegrenzt (Enterprise/Admin).

Das Erstellen eines Watchers verbraucht weder eine Konvertierung noch eine V2-Operation — Watch ist dadurch gated, wie viele Watcher du hältst, nicht durch einen Zähler pro Aufruf. Weil jeder Watcher in einem stündlichen-oder-langsameren Takt prüft, ist das gesamte Prüfvolumen eines Projekts durch max_watchers und den stündlichen Mindestwert zusammen begrenzt. Die aktuellen max_watchers-Kontingente pro Plan stehen auf der Preisseite.


Fehlerantworten

Status Bedingung
400 Bad Request Die URL löst zu einer privaten, Loopback-, Link-Local- oder Metadaten-Adresse auf (SSRF-Schutz zum Erstellungszeitpunkt).
401 Unauthorized Fehlender oder ungültiger API-Schlüssel / JWT-Token.
402 Payment Required Watch ist in deinem Plan nicht aktiviert, oder deine Anzahl aktiver Watcher hat max_watchers erreicht (auch bei einem paused→active-Fortsetzen durchgesetzt).
403 Forbidden /v2/watch ist nicht unter den erlaubten Endpunkten des API-Schlüssels.
404 Not Found Unbekannte watcher_id, eine, die einem anderen Projekt gehört, oder eine bereits soft-gelöschte.
422 Unprocessable Entity frequency_minutes unter 60 oder über 43200, ein leerer PATCH-Body ({}), ein ungültiges Enum in diff_mode oder status, eine url/webhook_url, die nicht http(s) ist, oder ein beliebiges unbekanntes Feld.
500 Internal Server Error Der Watcher konnte nicht erstellt werden. Erneut versuchen.

Die vollständige Referenz der Statuscodes steht in der Fehlercodes-Anleitung.


Limits

Limit Wert
URL-Länge 2.048 Zeichen
webhook_url-Länge 2.048 Zeichen
frequency_minutes 60–43.200 (1 Stunde bis 30 Tage)
Stündlicher Mindestwert 60 Minuten, durchgesetzt beim Erstellen, Aktualisieren und Planen
Poll-Intervall 60 Sekunden (eine Prüfung feuert innerhalb einer Minute nach ihrer geplanten Zeit)
Aufeinanderfolgende Fehler vor Auto-Pause 3
Render-Qualitäts-Mindestwert (kein Diff darunter) 0.4
Schwellwert für Textänderungs-Ähnlichkeit 0.98
Unified-Text-Diff gedeckelt auf 100 Zeilen
Änderungsdatensätze pro Diff gedeckelt auf 500
Stringwert pro Änderung gekürzt auf 2.000 Zeichen
Verglichener erfasster Textbody gedeckelt auf 200.000 Zeichen
Listen-/Snapshot-Seitengröße Standard 20, Maximum 100
Aktive Watcher pro Projekt planabhängig (max_watchers)