Polling war gestern: So baust du Delta-Sync und Webhooks richtig
Ein Flow, der jede Minute alle Aufträge abruft, läuft in der Testphase unauffällig. Im Dauerbetrieb taucht er im Request-Log als Lastspitze auf, und mit jedem weiteren Flow wird sie größer. Dieser Artikel zeigt den Gegenentwurf: Du holst nur ab, was sich geändert hat (Delta-Sync), und lässt dich von weclapp benachrichtigen, wenn etwas passiert (Webhooks). Dazu kommen die Muster, die beides im Betrieb stabil halten.
Den Anstoß gab Christian Lanig (weclapp): Er hat am Community Day 2026 im Vortrag „Jeder Request zählt“ gezeigt, wie man aus dem Request-Log konkrete Optimierungen ableitet. Den Rest liefert unsere Praxis. Wir betreiben Integrationen auf der weclapp API im Dauerbetrieb, mit Fulfillment 4 weclapp, 4Fulfillment, wals.pro AI 4 weclapp und n8n-Flows für unsere Kund:innen. Die Muster in diesem Artikel kommen aus Integrationen, die rund um die Uhr laufen müssen.
Vom Request-Log zur Optimierung: drei Symptome, drei Lösungen
Lanig ordnet in seinem Vortrag jedem Muster, das im Log sichtbar wird, eine Ursache und eine Lösung zu:
| Symptom im Log | Ursache | Lösung |
|---|---|---|
| Lastspitzen | Vollabgleich | Delta-Sync |
| Viele gleichartige Aufrufe | Polling | Webhooks |
| Viele Einzelrequests | N+1 | referencedEntities |

Die ersten beiden Zeilen sind das Thema dieses Artikels, die dritte bekommt einen eigenen Teil der Serie. Auch weclapp selbst empfiehlt: kein ungefiltertes Polling, idealerweise eine ereignisgesteuerte Architektur mit Webhooks.
Ein Rechenbeispiel zeigt, warum. Ein Abruf alle fünf Minuten sind 288 Requests pro Tag und Entität, egal ob sich etwas geändert hat. Bei zehn Entitäten und fünf Flows sind das 14.400 Requests am Tag, in denen oft nichts Neues steht. Die Last entsteht aus Anzahl, Parallelität und Dauer der Requests, und jeder Abruf wiegt umso schwerer, je größer die Liste ist (mehr dazu in Teil 1 zum Lastmanagement).
Delta-Sync: nur holen, was sich geändert hat
Jede weclapp-Entität trägt das schreibgeschützte Feld lastModifiedDate, einen Zeitstempel in Epoch-Millisekunden. Mit dem Operator ge fragst du alles ab, was seit einem Zeitpunkt angefasst wurde:
GET https://<dein-mandant>.weclapp.com/webapp/api/v2/article?lastModifiedDate-ge=1783634400000&properties=id,articleNumber,name,lastModifiedDate&pageSize=1000
AuthenticationToken: <dein-token>
Accept: application/json
Die Parameterwerte stehen hier unkodiert, damit man sie lesen kann; dein HTTP-Client kodiert sie beim Senden. properties und pageSize=1000 halten den Request schmal (Standard sind 100 Treffer, in der Regel maximal 1000). Damit steht der Kern. Ob dein Delta vollständig ist, entscheidet sich aber an zwei Stellen: am Cursor und am Blättern.
Der Cursor: Startzeit statt Abschlusszeit
Der naheliegende Cursor ist die Abschlusszeit des letzten Laufs. Er verliert Änderungen, die während des Laufs passieren:
11:00 Lauf startet, Cursor aus dem letzten Lauf: 10:00
11:01 Datensatz A wird gelesen
11:02 Datensatz A wird geändert
11:04 Lauf endet, neuer Cursor = 11:04 (Abschlusszeit)
11:05 Nächster Lauf fragt lastModifiedDate-ge=11:04 und A (11:02) fehlt
Robuster ist ein Fenster, dessen Obergrenze die Startzeit des Laufs ist. Der Lauf verarbeitet [Cursor, Start) und setzt den Cursor erst danach auf die Startzeit, und nur nach vollständigem Erfolg. Die Regeln dazu:
-
geist inklusiv. Damit bleiben Änderungen auf demselben Millisekundenwert im Fenster. - Ein Teilfehler lässt den Cursor stehen. Der nächste Lauf wiederholt das Fenster.
- Dein Handler arbeitet idempotent (Upsert statt Insert), denn bewusste Überlappung erzeugt Dubletten. Ein paar Minuten Überlappung fangen kleine Uhrabweichungen ab und Datensätze, die in Listen kurz verspätet sichtbar werden.
- Alternativ nimmst du als Cursor das Maximum von
lastModifiedDateder verarbeiteten Datensätze. Dann kommt der Wert vom weclapp-Server, und deine Uhr spielt keine Rolle. - Reiche den Cursor als reine Zahl durch, ohne Umweg über Datumsobjekte (siehe Teil 9 zu den Datentypen).
lastModifiedDate zählt die Millisekunden seit dem 1.1.1970 in UTC. Dein Cursor und Date.now() sprechen also dieselbe Sprache.
Blättern, ohne Datensätze zu verlieren
page und pageSize sind Offset-Paginierung, ein Snapshot entsteht dabei nicht. Das reicht für ruhige Bestände. In einem Bestand, der sich während des Laufs bewegt, entsteht eine Lücke: Du hast Seite 1 gelesen, ein Datensatz davon wird geändert und wandert ans Ende der Sortierung, alle Folgedatensätze rücken um eins vor, und der erste von Seite 2 rutscht auf Seite 1. Er wird nie gelesen.
Robuster ist Blättern nach ID mit festem Fenster. Du sortierst nach id und fragst die jeweils nächste Seite mit id-gt=<letzte ID>. Die Obergrenze lastModifiedDate-lt hält das Fenster stabil. Datensätze, die während des Laufs geändert werden, landen im nächsten Fenster.
const OVERLAP_MS = 5 * 60 * 1000; // Beispielwert, passe ihn an deine Latenz an
const PAGE_SIZE = 1000;
async function deltaRun(store, weclapp, handle) {
const { cursor } = await store.load(); // Ende des letzten erfolgreichen Fensters
const windowStart = cursor - OVERLAP_MS;
const windowEnd = Date.now(); // Startzeit dieses Laufs, nicht die Abschlusszeit
let lastId;
for (;;) {
const params = {
'lastModifiedDate-ge': windowStart,
'lastModifiedDate-lt': windowEnd,
sort: 'id',
pageSize: PAGE_SIZE,
properties: 'id,articleNumber,name,lastModifiedDate',
};
if (lastId) params['id-gt'] = lastId;
const { result } = await weclapp.get('/article', params);
for (const row of result) await handle(row); // idempotent
if (result.length < PAGE_SIZE) break;
lastId = result[result.length - 1].id;
}
await store.save({ cursor: windowEnd }); // erst nach vollständigem Erfolg
}
Das Keyset-Blättern über id-gt setzen wir in wals.pro AI 4 weclapp für Auswertungen ein. Unbekannte oder nicht filterbare Properties ignoriert weclapp ohne Fehlermeldung. Prüfe deshalb bei jedem neuen Filter die Trefferzahl.
Ein Delta kennt keine Löschungen
Gelöschte Datensätze tauchen in keinem Delta auf. Du hast zwei Wege. Der erste ist ein atDelete-Webhook (nächster Abschnitt). Der zweite ist ein gelegentlicher Vollabgleich der ID-Liste:
GET https://<dein-mandant>.weclapp.com/webapp/api/v2/article?properties=id&sort=id&pageSize=1000
AuthenticationToken: <dein-token>
Lokal löschst du dabei nur, wenn jede Seite fehlerfrei gelesen wurde. Bricht eine Seite ab, löschst du nichts. Sicherheitsrelevante Pfade lesen den Datensatz vor dem Handeln einzeln nach (GET /article/id/{id}), statt dem Cache zu vertrauen. So machen wir es in Fulfillment 4 weclapp: Der Artikelkatalog kommt stündlich als Delta in einen Cache mit 90 Minuten Gültigkeit.
Webhooks: weclapp ruft dich an
Statt zu fragen, lässt du dich benachrichtigen. In der Oberfläche richtest du das unter Globale Einstellungen > Integration > Webhooks ein. Per API ist webhook eine normale Entität. Anlegen geht so:
POST https://<dein-mandant>.weclapp.com/webapp/api/v2/webhook
AuthenticationToken: <dein-token>
Content-Type: application/json
{
"entityName": "shipment",
"atCreate": true,
"atUpdate": true,
"atDelete": false,
"requestMethod": "POST",
"url": "https://hooks.example.com/weclapp/<langes-zufaelliges-geheimnis>"
}
Pflichtfelder sind entityName, atCreate, atUpdate, atDelete, requestMethod (GET oder POST) und url. Die Antwort ist 201 mit dem angelegten Webhook samt id. Dazu gibt es GET /webhook, GET /webhook/id/{id}, PUT und DELETE. Die Rechte heißen entity-webhook-read, -update und -delete.
Das Modell ist bewusst klein: eine Entität, drei Schalter für Anlegen, Ändern und Löschen, genau eine Ziel-URL. Mehrere Empfänger bekommen je einen eigenen Webhook.
Was ankommt: ein Anstupsen, kein Datensatz
Der Webhook liefert nur drei Felder:
{
"entityId": "4711",
"entityName": "shipment",
"type": "UPDATE"
}
Mögliche Werte für type sind CREATE, UPDATE und DELETE. Du erfährst, dass etwas passiert ist, aber nicht was. Deshalb folgt immer ein Abruf:
GET https://<dein-mandant>.weclapp.com/webapp/api/v2/shipment/id/4711?properties=id,shipmentNumber,status,lastModifiedDate
AuthenticationToken: <dein-token>
Behandle den Payload nie als Wahrheit. Der Datensatz kann sich bis zu deinem Abruf schon wieder geändert haben, und bei DELETE gibt es nichts mehr zu lesen. Wenn du einen Flow löschst, bleibt seine Webhook-Registrierung in weclapp stehen und zeigt auf eine Karteileiche. Räume sie mit DELETE /webhook/id/{id} ab. Beim Umzug von einem System auf ein anderes, etwa von Power Automate auf n8n, hängst du die URL um, deaktivierst das alte System, beobachtest ein paar Werktage und löschst dann das alte System samt Webhook-Eintrag. Nie hängen zwei Systeme gleichzeitig am selben Webhook, sonst entstehen doppelte Nachrichten, zum Beispiel an einen Marktplatz.
Retry und Deaktivierung
Ist dein Endpunkt nicht erreichbar, versucht weclapp es nach 1, 5 und 30 Minuten sowie nach 5 Stunden und 1 Tag erneut. Ist er danach immer noch nicht erreichbar, wird der Webhook deaktiviert, und in der Webhook-Liste steht eine Fehlermeldung.
Daraus folgen zwei Regeln. Erstens quittierst du schnell mit 200 und arbeitest außerhalb des Requests weiter. Zweitens überwachst du deine Webhooks. Das Schema kennt dafür deactivatedDate und errorMessage:
GET https://<dein-mandant>.weclapp.com/webapp/api/v2/webhook?properties=id,entityName,url,deactivatedDate,errorMessage
AuthenticationToken: <dein-token>
Ein Webhook mit gesetztem deactivatedDate löst einen Alarm aus. Das Fallback-Netz weiter unten fängt in der Zwischenzeit auf, was dir entgangen ist.
Absichern ohne Signatur
Eine Signaturprüfung wie bei anderen Plattformen bieten weclapp-Webhooks nicht, es gibt kein Feld für ein Geheimnis oder einen Signatur-Header. Das lässt sich mit einfachen Mitteln auffangen:
-
Geheimnis im URL-Pfad: lang, zufällig, nur über HTTPS. Bei falschem Pfad antwortet dein Endpunkt mit
404. -
Die URL wie ein Passwort behandeln: Wer
GET /webhooklesen darf, sieht sie. - Dem Payload misstrauen: Der Folgeabruf läuft mit deinem eigenen Token. Ein gefälschtes Event kostet im schlimmsten Fall einen überflüssigen Lesezugriff.
const { timingSafeEqual } = require('node:crypto');
const SECRET = Buffer.from(process.env.HOOK_SECRET);
function receiver(queue) {
return (req, res) => {
const given = Buffer.from(req.params.secret || '');
const ok = given.length === SECRET.length && timingSafeEqual(given, SECRET);
if (!ok) return res.sendStatus(404); // verrät nicht, dass der Pfad existiert
res.sendStatus(200); // sofort quittieren
queue.push(req.body); // { entityId, entityName, type }
};
}
In n8n erledigst du dasselbe mit einem Webhook-Node, dessen Pfad das Geheimnis trägt und der sofort antwortet.
Webhook-Stürme und Endlosschleifen
Für weclapp ist ein Geschäftsvorgang selten ein einzelnes Ereignis. Im Versandhandel kann ein einzelner Auftrag innerhalb weniger Sekunden ein CREATE und drei UPDATE auslösen. Startet jedes Event einen eigenen Flow-Lauf, sehen die Läufe unterschiedliche Zwischenzustände und scheitern an Race-Fehlern wie „create standard sales invoice is not possible“.
Dazu kommt ein zweiter Effekt: Deine eigenen Writes lösen Events aus. Legt dein Flow eine Lieferung an oder setzt einen Status, ändert sich der Auftrag, weclapp feuert den nächsten Webhook, und dein Flow läuft wieder:
Auftrag geändert --Webhook--> dein Flow
^ |
'---- PUT Status / createShipment ----'
Plane das von Beginn an ein. Eine Wartezeit von ein paar Sekunden hilft als Notbehelf, löst das Problem aber nicht. Was es wirklich löst, zeigen die folgenden fünf Muster.
Fünf Muster, die Stürme beherrschbar machen
1. Quittieren, dann arbeiten. Der Endpunkt nimmt nur an und legt das Event in eine Queue. Queues liefern in der Regel mindestens einmal aus, also kommen Dubletten vor. In 4Fulfillment geht jedes Event über eine Queue mit Dead-Letter-Topic nach fünf Zustellversuchen, und die Parallelität ist je Handler festgelegt: zehn parallele Läufe für die Label-Erzeugung, genau einer für den Artikelsync.
2. Früh aussteigen. Der Handler liest die Entität frisch und prüft Status und Typ, bevor er irgendetwas tut. Verzweigungen steuerst du über Felder des frisch gelesenen Datensatzes, nicht über das Ergebnis übersprungener Schritte.
3. Zustand am Datensatz speichern. Ein Zusatzfeld vom Typ Checkbox („Label erstellt“, „Label gedruckt“) sagt dem nächsten Lauf, was schon erledigt ist. Setzt ein:e Anwender:in das Häkchen zurück, verarbeitet der nächste Webhook den Vorgang neu, ganz ohne Code. Wie du Zusatzfelder sauber liest und schreibst, steht in Teil 9 zu den Datentypen.
4. Lease-Lock mit Double-Check. Pro Entität gibt es einen Lock mit Ablaufzeit (bei uns fünf Minuten). Nach dem Erwerb liest der Handler noch einmal, denn zwischen erster Prüfung und Lock kann ein anderer Lauf fertig geworden sein. Freigegeben wird im finally.
async function handleEvent(evt, { weclapp, locks, allowedStatus }) {
const { entityId, type } = evt;
if (type === 'DELETE') return; // nichts mehr zu lesen
const read = () => weclapp.get(`/shipment/id/${entityId}`);
const isDone = (s) => s.customAttributes?.some(
(a) => String(a.attributeDefinitionId) === DONE_FLAG_ID && a.booleanValue);
const shipment = await read(); // Payload nie als Wahrheit
if (!allowedStatus.includes(shipment.status) || isDone(shipment)) return;
const lock = await locks.acquire(`shipment:${entityId}`, { leaseMs: 5 * 60_000 });
if (!lock) return; // eine andere Instanz arbeitet daran
try {
const fresh = await read(); // Double-Check nach dem Lock
if (isDone(fresh)) return;
await doWork(fresh);
await weclapp.put(`/shipment/id/${entityId}?ignoreMissingProperties=true`, {
customAttributes: [{ attributeDefinitionId: DONE_FLAG_ID, booleanValue: true }],
});
} finally {
await lock.release();
}
}
Bei einem Fehler schreibst du nur das Teilergebnis zurück (nur Erfolgreiches) und protokollierst den Fehler als Kommentar am Beleg, damit Anwender:innen sehen, was passiert ist.
5. Idempotenz ab dem ersten Entwurf. Bevor dein Flow etwas anlegt, prüft er, ob es das schon gibt:
GET https://<dein-mandant>.weclapp.com/webapp/api/v2/shipment?mainSalesOrderId-eq=<auftrags-id>&properties=id,status
AuthenticationToken: <dein-token>
Prüfe die Trefferzahl einmal per /count: Einen Filter, den weclapp nicht kennt, ignoriert die API still und liefert dann alle Lieferungen.
Für Fälle, in denen du Läufe gezielt bremsen willst, gibt es drei Hebel mit jeweils einem Preis:
| Hebel | Gewinn | Preis |
|---|---|---|
| Trigger-Concurrency 1 | Läufe laufen nacheinander | Durchsatz sinkt, Umkehr nur per Redeploy |
Dedup-Gate über lastModifiedDate oder version
|
Dubletten fallen weg | Zu grob, eine echte Folgeänderung wird verworfen |
| „Schon passiert“ gilt als Erfolg | Race-Fehler verschwinden | Echte Fehler werden maskiert |
Idempotenz ist die Lösung, die ohne diesen Preis auskommt.
Fallback: Webhook als Auslöser, Delta als Sicherheitsnetz
Webhooks können verloren gehen. Dein Endpunkt ist länger als einen Tag weg, ein Deployment hängt, der Webhook wurde deaktiviert. Deshalb bekommt jede Webhook-Integration ein Netz: einen geplanten Lauf, der mit dem Delta aus dem ersten Teil des Artikels alles findet, was die Webhooks nicht erledigt haben. Die Verarbeitung ist dieselbe. Der Webhook ruft den Handler mit einer ID auf, der Schedule mit allen IDs des Deltas. Dank Statusfilter, Zustandsflag und Lock ist doppelte Verarbeitung harmlos. Ein Takt von zum Beispiel 15 Minuten ist ein Ausgangswert, den du an deinen Prozess anpasst.

Ein leeres Ergebnis ist kein Erfolg
Poll-und-Filter-Flows scheitern gern lautlos. Eine Abfrage ohne Treffer meldet Erfolg, auch wenn die Vorstufe aufgehört hat, Datensätze zu markieren. Dann bleiben Aufträge liegen, während n8n keinen einzigen Fehler zeigt. Genauso tückisch: Steht an einem entscheidenden weclapp-Write-Node „bei Fehler weitermachen“, feuert der Error-Workflow nie. Prüfe deshalb bei jedem bestehenden Flow, wo diese Option gesetzt ist.
Die Gegenmaßnahme ist eine Erwartungsprüfung statt nur Fehlerbehandlung: „Seit zwei Stunden keine Treffer verarbeitet, obwohl Kandidaten offen sind.“
GET https://<dein-mandant>.weclapp.com/webapp/api/v2/shipment/count?status-eq=DELIVERY_NOTE_PRINTED&warehouseId-eq=<deine-lager-id>
AuthenticationToken: <dein-token>
Liefert count offene Kandidaten, aber seit Stunden hat kein Lauf etwas verarbeitet, schlägt der Alarm an. Entprelle die Alarme: Fingerprint plus Datum, eine Mail pro Fehler und Tag. Ein Dauerfehler im 15-Minuten-Takt sind sonst 96 Mails am Tag.
Webhook oder Schedule?
Beides hat seinen Platz:
| Kriterium | Webhook | Schedule mit Delta |
|---|---|---|
| Reaktionszeit | Sekunden | Takt des Schedules |
| Typischer Einsatz | Einzelne Geschäftsereignisse (Versand auslösen, Benachrichtigung) | Stammdaten-Sync, Preise, Berichte |
| Volumen | Gering bis mittel | Hoch, auch Massenimporte |
| Löschungen | atDelete |
Nur über Vollabgleich der IDs |
| Eigene Writes | Erzeugen Folge-Events | Keine Rückkopplung |
| Ausfall | Events können fehlen | Holt beim nächsten Lauf nach |
Bei hohem Volumen ist der Schedule die ruhigere Wahl, weil jedes Event sonst einen eigenen Lauf mit Folgeabruf startet. Vor Massenänderungen oder Backfills pausierst du die Webhook-Verarbeitung und den Fallback-Lauf, damit dein eigener Flow nicht in die Änderungswelle hineinläuft.
Checkliste
- Kein ungefiltertes Intervall-Polling: Listen holst du mit
lastModifiedDate-geundproperties. - Cursor = Startzeit des Laufs (oder Maximum von
lastModifiedDate), nie die Abschlusszeit. Der Cursor rückt erst nach vollständigem Erfolg vor. - Überlappung eingeplant, Verarbeitung idempotent.
- Blättern nach
id-gtbei großen Deltas, Trefferzahl percountgegengeprüft. - Löschungen über
atDeleteoder über einen vollständig gelesenen ID-Abgleich abgedeckt. - Webhook-Endpunkt: Geheimnis im Pfad, sofort
200, Verarbeitung dahinter in einer Queue. - Handler liest die Entität frisch, steigt früh aus, schützt sich mit Zustandsflag und Lease-Lock mit Double-Check.
- Eigene Writes lösen keine Endlosschleife aus, weil der nächste Lauf nichts mehr findet, was zu tun ist.
- Fallback-Schedule mit Delta vorhanden, Erwartungsprüfung und entprellter Alarm eingerichtet.
-
deactivatedDatedeiner Webhooks wird überwacht, Karteileichen werden abgeräumt.
Weiterlesen und Ausprobieren
Dieser Artikel gehört zur Serie „weclapp API: Das Handbuch“. Weiter geht es mit Teil 1 zum Lastmanagement und Teil 9 zu den Datentypen. Die Endpunkte aus diesem Artikel kannst du in unserer kostenlosen Postman Collection 4 weclapp gegen einen Testmandanten ausprobieren. Eine Stunde Grundlagen zu Token, Filtern und Entitäten gibt es im Webinar „weclapp API Deep Dive“ auf YouTube. Und wenn du eine Integration stabilisieren willst, sprechen wir im Vorgespräch darüber: wals.pro/termin