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/watchist ein V2-Endpunkt mit eigenem Plan-Gate, getrennt von V1-Konvertierungen. Wie viele aktive Watcher du gleichzeitig halten kannst, ist durchmax_watchersin deinem Plan begrenzt, und ein Plan, in dem Watch deaktiviert ist, kann gar keinen erstellen — beide Ablehnungen kommen als402zurü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:
- 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), einewat_-ID wird erzeugt, und der Watcher wirdactivemit auf jetzt gesetztemnext_check_atgespeichert. - Beanspruchen. Ein droplet-lokaler
watch_workerscannt alle 60 Sekunden nach aktiven Zeilen, derennext_check_atverstrichen ist. Er beansprucht sie unterFOR UPDATE SKIP LOCKEDund 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. - 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.
- 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.
- Benachrichtigen. Wenn der Diff eine Änderung meldet, feuern der
HMAC-signierte Webhook (falls gesetzt) und die Eigentümer-E-Mail (falls
notify_emailaktiviert ist) gleichzeitig, nach Best-Effort. - 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 60–43200-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- undafter-Werte innerhalb vonchangessind 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_atist gesetzt.paused— außerhalb des Zeitplans (next_check_atistnull). Erreicht entweder durchPATCH {"status": "paused"}oder automatisch nach drei aufeinanderfolgenden Render-Fehlern.deleted— terminaler Grabstein, nur überDELETEerreichbar. Die Zeile bleibt erhalten (sodass der Prüfverlauf überlebt), erscheint aber nie in Listen und liest sich als404auf 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 vieleactive-Watcher ein Projekt gleichzeitig halten darf. Pausierte und gelöschte Watcher zählen nicht. Ein Wert von0bei 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) |