Website-Änderungserkennung API#

Private Beta. Watch ist heute mit deinem normalen API-Schlüssel in jedem kostenpflichtigen Tarif aufrufbar, und Watcher kosten keine Ops; im kostenlosen Founding-Tarif lassen sie sich gar nicht anlegen. Angekündigt oder allgemein verfügbar ist Watch nicht: Request- und Response-Formen können sich jederzeit ohne Vorankündigung ändern, und es gibt keine Stabilitäts- oder Supportzusage, baue also noch nichts Tragendes darauf. Die Roadmap steht unter Demnächst, und jedes Release wird im Changelog angekündigt.

POST /v2/watch ist eine API zur Erkennung von Website-Änderungen: Du registrierst eine Seite einmal, und ein droplet-lokaler Scheduler rendert sie in festem Takt in echtem Headless Chrome neu, vergleicht jede Capture per Diff mit der vorherigen und benachrichtigt dich (per HMAC-signiertem Webhook, E-Mail oder beidem), wenn sich die Seite tatsächlich ändert. Es wird den Cron-Job plus Diffing plus Alerting- Unterbau ersetzen, den du sonst rund um den perceive-Endpunkt aufbauen müsstest: ein Datensatz statt Scheduler, Storage-Bucket und Vergleichsskript.

Hier ist der kleinste sinnvolle Aufruf. Sende eine URL und erhalte einen Watcher zurück, der stündlich geplant ist:

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

Die Antwort ist der vollständige Watcher-Datensatz. Er ist sofort active, und next_check_at wird 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
}

Endpoints#

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} Löscht einen Watcher weich (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 unten.

X-API-Key: sk_your_private_key

Öffentliche Schlüssel mit einem JWT-Bearer-Token funktionieren ebenfalls, nach demselben Ablauf wie bei jedem anderen Endpunkt: Erzeuge ein Token mit deinem pk_-Schlüssel und sende es dann als Authorization: Bearer <token>. Der vollständige Ablauf, einschließlich Domain-Sperre und Token-Refresh, steht im Authentifizierungs-Leitfaden.

Jeder API-Schlüssel hat eine Allowlist erlaubter Endpunkte. Steht /v2/watch nicht auf der Liste des Schlüssels, wird die Erstellungsanfrage mit 403 abgelehnt. Sobald ein Schlüssel Watcher erstellen darf, erreicht er auch die zugehörigen Pro-Watcher-Routen: GET, PATCH und DELETE auf /v2/watch/{watcher_id} sowie dessen /snapshots-Seite werden automatisch durchgelassen, sodass du nicht jedes Verb einzeln freigeben musst.


Wie watch funktioniert#

Dahinter steckt keine externe Queue: kein Google Cloud Tasks, kein Drittanbieter-Scheduler. Der Zeitplan liegt vollständig in der Datenbankspalte next_check_at, und ein prozessinterner Poller steuert ihn:

  1. Erstellen. Du sendest per POST eine URL. Die URL wird auf SSRF geprüft (ein privater, Loopback- oder Metadata-Host wird abgelehnt, bevor eine Zeile geschrieben wird), eine wat_-ID wird erzeugt, und der Watcher wird active gespeichert, wobei next_check_at auf jetzt gesetzt wird.
  2. Beanspruchen. Ein droplet-lokaler watch_worker durchsucht alle 60 Sekunden aktive Zeilen, deren next_check_at überschritten ist. Er beansprucht sie unter FOR UPDATE SKIP LOCKED und rückt den Zeitplan jeder einzelnen um ein volles Intervall in derselben Transaktion vor, sodass ein langsames Rendering nie doppelt beansprucht wird und ein Absturz mitten im Rendering lediglich einen Zyklus überspringt.
  3. Rendern. Jeder beanspruchte Watcher wird einmal über das gemeinsame Headless-Chrome-Singleton gerendert, dieselbe Capture-Pipeline, die auch hinter dem perceive-Endpunkt steht. Das Rendering ist credential-frei: Es werden keine Auth-Daten, Cookies oder Header gespeichert, sodass für die wiederkehrende Prüfung nichts Geheimes ruhend gespeichert wird.
  4. Bewerten und Diffen. Ein Rendering, das unter der Qualitätsschwelle (0.4) liegt oder als blockiert markiert ist, wird als reine Audit-Prüfung erfasst, ohne Content-Hash, sodass es nie zur Diff-Baseline wird und nie eine Benachrichtigung auslöst. Ein gutes Rendering wird in eine Capture umgewandelt (Haupttext plus extrahierte Struktur), gegen die letzte gute Capture per Diff verglichen, und das Ergebnis wird in eine Snapshot-Zeile geschrieben.
  5. Benachrichtigen. Meldet der Diff eine Änderung, lösen der HMAC-signierte Webhook (falls gesetzt) und die E-Mail an den Owner (falls notify_email aktiv ist) gleichzeitig aus, nach bestem Bemühen (best-effort).
  6. Neu planen. Der Worker schreibt das nächste next_check_at. Drei aufeinanderfolgende Render-Fehlschläge pausieren den Watcher und benachrichtigen den Owner per E-Mail, statt neu zu planen.

Da der Zeitplan eine Datenbankspalte ist, braucht Downtime keine Recovery-Logik: Der erste Tick nach dem Start räumt 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. Max. 2,048 Zeichen. Erforderlich.
frequency_minutes integer 60 Minuten zwischen den Prüfungen. Harte Stundenuntergrenze: Minimum 60, Maximum 43200 (30 Tage).
diff_mode string "auto" Welche Diff-Strategie angewendet wird. auto, text, structured, tables oder metadata. Siehe Diff-Modi.
track_fields object null Optionale Teilmenge von Feldern/Selektoren, 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 auf SSRF geprüft. Max. 2,048 Zeichen; muss http(s) sein.
notify_email boolean true Benachrichtigt den Projekt-Owner per E-Mail bei einer erkannten Änderung und bei Auto-Pause.

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

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

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

Parameter Typ Beschreibung
frequency_minutes integer Neuer Takt. Dieselben Grenzen von 60 bis 43200 wie beim Erstellen.
diff_mode string Wechselt die Diff-Strategie.
track_fields object Ersetzt die Teilmenge der verfolgten Felder.
webhook_url string Setzt einen neuen Webhook. Ein leerer String ist das explizite Signal zum Löschen und speichert NULL.
notify_email boolean Schaltet die Owner-E-Mail um.
status string active oder paused. Fortsetzen aktiviert den Zeitplan neu (next_check_at wird auf den nächsten Tick gesetzt); Pausieren leert ihn, sodass der Poller die Zeile nicht mehr beansprucht.

Ein leerer Body ({}) wird mit 422 abgelehnt, statt stillschweigend zu einem No-op zu werden. Beachte, dass status hier nur active oder paused akzeptiert. Der finale deleted-Zustand wird ausschließlich über DELETE erreicht, nie über PATCH. Das Fortsetzen eines pausierten Watchers zählt wie das Hinzufügen eines aktiven Monitors und prüft daher erneut gegen dieselbe max_watchers-Grenze wie beim Erstellen und kann 402 zurückgeben.


Diff-Modi#

diff_mode wählt aus, 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 beschränken den Diff auf eine einzelne Strategie.

diff_mode Strategie Was erkannt wird
auto (Standard) Alle vier unten Jede Art von Änderung in einem Durchgang.
text SequenceMatcher-Ratio des Haupttexts Textkörper hat sich geändert; wird markiert, wenn die Ähnlichkeit unter 0.98 fällt, sodass ein umgestelltes Wort oder eine Whitespace-Änderung den Watcher nicht flattern lässt. Enthält einen Unified Diff, begrenzt auf 100 Zeilen.
structured Keyed-List-Matching Hinzugefügte / entfernte / pro Feld geänderte Einträge bei Links (abgeglichen über href) und JSON-LD-Blöcken (abgeglichen über @type + name). Reihenfolge spielt keine Rolle.
tables Kontext-Überschriften-Matching Tabellen werden über Beschriftung/Überschrift abgeglichen; meldet Änderungen der Zeilenanzahl, hinzugefügte/entfernte Tabellen und Inhaltsänderungen bei gleicher Zeilenanzahl (begrenzt auf 100 Zeilen Kontext).
metadata Schlüssel-für-Schlüssel-Dict-Vergleich Hinzugefügte, entfernte und geänderte Seiten-Metadatenfelder.

Unabhängig vom gewählten Modus ist das similarity des Snapshots immer die gesamte Ratio der kompletten Capture (0.0 bis 1.0). Bei einem eingeschränkten Modus kann das bedeuten, dass similarity niedrig ausfällt, während has_changes false ist, weil ein Abschnitt, den du nicht diffst, sich verändert hat, während sich in deiner gewählten Strategie nichts geändert hat.

Eine Teilmenge von Feldern verfolgen#

track_fields beschränkt den Diff auf Änderungen, deren Abschnitt, Feld oder Schlüssel mit einem verfolgten Begriff übereinstimmt. Es akzeptiert ein Objekt, dessen Schlüssel sowie alle Listenwerte zu den verfolgten Begriffen werden. So verfolgt {"metadata": ["title"]} sowohl den Abschnitt metadata als auch das Feld title. Der Abgleich erfolgt Token-genau, nicht als Teilstring: Ein Begriff price trifft 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 Pro-Watcher-Routen.
url string Die überwachte URL.
status string active, paused oder deleted.
frequency_minutes integer Aktueller Takt, nach Anwendung der Stundenuntergrenze.
diff_mode string Die aktive Diff-Strategie.
track_fields object Die Teilmenge der verfolgten Felder, oder null.
webhook_url string Der Änderungs-Webhook, oder null.
notify_email boolean Ob die Owner-E-Mail aktiv ist.
consecutive_errors integer Aufeinanderfolgende Render-Fehlschläge. Wird bei einer erfolgreichen Prüfung auf 0 zurückgesetzt; bei 3 pausiert der Watcher automatisch.
checks_count integer Gesamtzahl durchgeführter Prüfungen, erfolgreich oder fehlgeschlagen.
last_check_at string UTC-Zeitstempel der letzten Prüfung, oder null.
next_check_at string UTC-Zeitstempel der nächsten geplanten Prüfung. null bei pausiert oder gelöscht.
last_change_at string UTC-Zeitstempel der zuletzt erkannten Änderung, oder null.
created_at string Wann der Watcher erstellt wurde.
updated_at string Letzte Änderung, oder null, falls nie aktualisiert.

GET /v2/watch gibt pro Zeile ein kompaktes WatcherSummary zurück (es lässt diff_mode, track_fields, webhook_url, notify_email und updated_at weg), eingebettet in eine Page-Envelope:

Feld Typ Beschreibung
watchers array Die Seite der Summaries, neueste zuerst.
skip integer Der von dir angeforderte Offset.
limit integer Die aktuell geltende Seitengröße.
has_more boolean true, wenn über diese Seite hinaus weitere Watcher existieren.

Snapshot-Verlauf#

GET /v2/watch/{watcher_id}/snapshots gibt die Prüf-Timeline des Watchers zurück, neueste zuerst. Jeder Eintrag enthält das Diff-Ergebnis. Der eigentliche Snapshot-Capture-Inhalt 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 Gesamte Ratio der kompletten Capture, 0.0 bis 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.

Wichtiger Hinweis. Die Werte before und after 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 String-Werte sind bereits auf 2,000 Zeichen gekürzt, und ein einzelner Diff ist auf 500 Änderungsdatensätze begrenzt.


Lebenszyklus und Zeitplanung#

Ein Watcher durchläuft drei Zustände:

  • active: auf dem Zeitplan des Pollers. next_check_at ist gesetzt.
  • paused: nicht mehr im Zeitplan (next_check_at ist null). Wird entweder über PATCH {"status": "paused"} erreicht oder automatisch nach drei aufeinanderfolgenden Render-Fehlschlägen.
  • deleted: finaler Tombstone, nur über DELETE erreichbar. Die Zeile bleibt erhalten (sodass der Prüfverlauf erhalten bleibt), erscheint aber nie in Listen und liefert 404 auf den Pro-Watcher-Routen.

Die Stundenuntergrenze wird an zwei Stellen durchgesetzt: beim Erstellen/Aktualisieren und erneut vom Scheduler, wenn er next_check_at vorrückt. So kann selbst eine Zeile, deren Takt direkt in der Datenbank bearbeitet wurde, die Untergrenze nie unterschreiten.

Auto-Pause#

Nach drei aufeinanderfolgenden Render-Fehlschlägen wird der Watcher auf paused gesetzt, sein Zeitplan wird geleert, und falls notify_email aktiv ist, erhält der Owner eine E-Mail zum pausierten Watcher. Eine einzelne 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, wodurch next_check_at für den nächsten Tick neu aktiviert wird.

Löschen#

DELETE /v2/watch/{watcher_id} ist ein Soft-Delete: Es setzt status auf deleted, leert next_check_at und gibt den tombstoned Datensatz mit 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-Kontingent (nur active-Watcher zählen).


Codebeispiele#

curl: Erstellen mit Standardwerten#

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: alle 6 Stunden, Webhook plus Tabellen-Tracking#

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: Pausieren, dann fortsetzen#

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);

Fehlerantworten#

Status Bedingung
400 Bad Request Die URL löst zu einer privaten, Loopback-, Link-Local- oder Metadata-Adresse auf (SSRF-Schutz beim Erstellen).
401 Unauthorized Fehlender oder ungültiger API-Schlüssel / JWT-Token.
402 Payment Required Watch ist auf deinem Plan nicht aktiviert, oder deine Anzahl aktiver Watcher hat max_watchers erreicht (wird auch beim Fortsetzen von pausiert→aktiv durchgesetzt).
403 Forbidden /v2/watch gehört nicht zu 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ültiger Enum-Wert 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 Statuscode-Referenz findest du im Fehlercode-Leitfaden.


Limits#

Limit Wert
URL-Länge 2,048 Zeichen
Aktive Watcher (max_watchers) 20 / 100 / 500 auf Indie / Studio / Production; Watch ist im Founding-Plan nicht verfügbar
Ops pro Prüfung 0 (Watcher schöpfen nie aus dem monatlichen Ops-Kontingent)
webhook_url-Länge 2,048 Zeichen
frequency_minutes 60 bis 43,200 (1 Stunde bis 30 Tage)
Stundenuntergrenze 60 Minuten, durchgesetzt beim Erstellen, Aktualisieren und bei der Zeitplanung
Poll-Intervall 60 Sekunden (eine Prüfung löst innerhalb einer Minute nach ihrer geplanten Zeit aus)
Aufeinanderfolgende Fehler vor Auto-Pause 3
Render-Qualitätsuntergrenze (kein Diff darunter) 0.4
Ähnlichkeitsschwelle für Textänderungen 0.98
Unified-Text-Diff begrenzt auf 100 Zeilen
Änderungsdatensätze pro Diff begrenzt auf 500
String-Wert pro Änderung gekürzt auf 2,000 Zeichen
Erfasster, per Diff verglichener Textkörper begrenzt auf 200,000 Zeichen
Seitengröße Liste/Snapshot Standard 20, max. 100

Häufig gestellte Fragen#

Wie richte ich Webhooks für die Überwachung von Website-Änderungen ein?#

Übergib webhook_url beim Erstellen des Watchers mit POST /v2/watch, oder füge es später über PATCH /v2/watch/{watcher_id} hinzu. Wenn eine Prüfung eine Änderung erkennt, sendet EnConvert einen HMAC-signierten POST an diese URL; das Ziel wird unmittelbar vor jeder Zustellung auf SSRF geprüft, und ein leerer String bei PATCH löscht ihn.

Wie oft kann die API eine Seite auf Änderungen prüfen?#

frequency_minutes legt den Takt fest, von 60 (die harte Stundenuntergrenze) bis 43200 (30 Tage). Der Poller durchsucht alle 60 Sekunden, sodass eine Prüfung innerhalb einer Minute nach ihrer geplanten Zeit auslöst.

Warum hat sich mein Watcher selbst pausiert?#

Drei aufeinanderfolgende Render-Fehlschläge pausieren einen Watcher automatisch, leeren seinen Zeitplan und benachrichtigen den Owner per E-Mail, falls notify_email aktiv ist. Setze ihn per PATCH mit {"status": "active"} zurück, um next_check_at neu zu aktivieren; eine einzelne erfolgreiche Prüfung setzt consecutive_errors auf 0 zurück.

Kann ich nur einen Teil einer Seite überwachen, z. B. einen Preis oder eine Tabelle?#

Ja. track_fields beschränkt den Diff auf Änderungen, deren Abschnitt, Feld oder Schlüssel mit einem verfolgten Begriff übereinstimmt (Token-genauer Abgleich, sodass price auf offers.price, aber nicht auf priceCurrency trifft), und diff_mode kann die Erkennung auf eine einzelne Strategie wie tables, structured, text oder metadata beschränken.