Blätterung: der Zeiger statt der Seitenzahl
Warum es kein `?page=` gibt, wie `nextCursor` benutzt wird, welche zwei Umschläge es gibt — und wie ein inkrementeller Abgleich aussieht, der nichts überspringt.
Für EntwicklerES GIBT KEINE SEITENZAHL UND KEIN `?offset=`. Jede Liste blättert über einen ZEIGER: die Antwort enthält `nextCursor`, und den schicken Sie unverändert als `?cursor=` an denselben Aufruf zurück. Ist `nextCursor` gleich `null`, sind Sie am Ende.
DER GRUND IST KEIN GESCHMACK. Während ein Abgleich Seite für Seite läuft, legt die Buchungsstrecke neue Zeilen an. Jede eingeschobene Zeile verschiebt alle folgenden Seiten um eins — der Abgleich ÜBERSPRINGT dann genau die Zeile an der Nahtstelle, und niemand merkt es. Ein Zeiger auf dem letzten Sortierwert kennt dieses Loch nicht. Dazu kommt der Preis: bei `OFFSET n` liest die Datenbank die übersprungenen Zeilen und wirft sie weg; Seite 40 kostet vierzigmal so viel wie Seite 1.
DER ZEIGER IST UNDURCHSICHTIG UND GEHÖRT NICHT AUSGEWERTET. Er trägt die Sortierwerte der zuletzt gelieferten Zeile UND einen Fingerabdruck über Sortierung und Filter. Ändern Sie zwischen zwei Seiten einen Filter oder die Sortierung, antwortet die API mit 400 statt mit einer Liste, die still aus zwei verschiedenen Abfragen zusammengesetzt ist. Er ist ausdrücklich KEIN Sicherheitsmerkmal: die Mandantengrenze hängt am Schlüssel, nicht am Zeiger.
ES GIBT ZWEI UMSCHLÄGE, und der Unterschied ist historisch, nicht bedeutungstragend. Die meisten Listen antworten flach: `{ "data": [...], "nextCursor": ..., "hasMore": ..., "limit": ... }`. Der Gästebereich fasst dasselbe in ein Unterobjekt: `{ "data": [...], "pagination": { "limit", "nextCursor", "hasMore" }, "meta": { "restaurantId", "timezone" } }`. Auf jeder Ressourcenseite steht, welcher der beiden gilt.
`limit` IST 50 IN DER VORGABE UND 200 IM HÖCHSTFALL. Es gibt kein „alles" — jede Abfrage der API trägt eine harte Obergrenze in der Abfrageschicht, und die kann nicht ausfallen (die Ratenbremse schon, siehe „Ratenbegrenzung").
`total` KOSTET EINE ZWEITE ABFRAGE und kommt deshalb nur auf Zuruf: `?includeTotal=true`. Ohne diesen Parameter steht das Feld nicht im Körper. Für einen Fortschrittsbalken ist es sinnvoll, für einen Abgleich ist es Verschwendung.
FÜR DEN ABGLEICH GIBT ES `updatedSince` (Reservierungen, Gäste, Stammdaten) bzw. `updatedAtFrom`/`updatedAtTo` (Warenwirtschaft). Merken Sie sich den höchsten `updatedAt`, den Sie gesehen haben, und fragen Sie beim nächsten Lauf ab genau diesem Zeitpunkt. Die Gästeliste ist dafür AUFSTEIGEND nach `updatedAt` sortiert: wird eine Zeile während des Blätterns geändert, wandert sie ans Ende und wird in DIESEM Durchlauf noch gelesen. Absteigend spränge sie hinter den Zeiger und fehlte bis zum nächsten Lauf.
Schritt für Schritt
Erste Seite holen
Ohne `cursor`, mit `limit` so gross wie sinnvoll (bis 200) und den Filtern, die für ALLE Seiten gelten sollen.
`data` verarbeiten
Die Zeilen stehen in `data`. Im Gästebereich zusätzlich `meta.restaurantId` prüfen, wenn Ihr System mehrere Häuser bedient.
`nextCursor` unverändert weiterreichen
Als `?cursor=<wert>` an denselben Aufruf, mit denselben Filtern und derselben Sortierung. Nicht zerlegen, nicht kürzen, nicht neu kodieren.
Beenden, wenn `nextCursor` null ist
Nicht auf eine leere `data`-Liste warten: die letzte volle Seite hat bereits `nextCursor: null`, ein weiterer Aufruf wäre umsonst.
Eine Obergrenze an Seiten einbauen
Eine `while`-Schleife ohne Deckel ist eine Endlosschleife, sobald irgendwo ein Zeiger stehen bleibt. Zehntausend Zeilen sind fünfzig Seiten; ein Deckel bei 1 000 Seiten kostet nichts und rettet den Nachtlauf.
Codebeispiele
# Erste Seite: gross anfragen, Filter setzen.curl -sS "https://tactictable.com/api/v1/reservations?date=2026-09-20&limit=200&sort=date&dir=asc" \ -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM" # Folgeseite: DERSELBE Aufruf plus cursor. Filter unveraendert lassen.curl -sS "https://tactictable.com/api/v1/reservations?date=2026-09-20&limit=200&sort=date&dir=asc&cursor=eyJ2IjoxLCJzb3J0IjoiZGF0ZSJ9" \ -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM"{ "data": [ { "id": "31061d44-ac33-550d-b4d0-a16973e270f5", "status": "CONFIRMED", "date": "2026-09-20", "time": "19:30", "partySize": 4, "guestName": "Familie Berger" } ], "nextCursor": "eyJ2IjoxLCJzb3J0IjoiZGF0ZSIsImRpciI6ImFzYyJ9", "hasMore": true, "limit": 200}{ "data": [ { "id": "fb69190b-e35f-5a2e-be8a-b7c272782903", "name": "Anna Berger", "email": "anna.berger@example.at", "totalVisits": 12, "updatedAt": "2026-09-14T07:12:44.019Z" } ], "pagination": { "limit": 200, "nextCursor": "MjAyNi0wOS0xNFQwNzoxMjo0NC4wMTlafGZiNjkxOTBi", "hasMore": true }, "meta": { "restaurantId": "35122c8e-d842-5b49-814c-0e8f2aaed157", "timezone": "Europe/Vienna" }}interface Seite<T> { data: T[] nextCursor: string | null hasMore: boolean limit: number total?: number} const MAX_SEITEN = 1000 export async function alleSeiten<T>( pfadMitFiltern: string, token: string,): Promise<T[]> { const alles: T[] = [] let cursor: string | null = null for (let seite = 0; seite < MAX_SEITEN; seite++) { const trenner = pfadMitFiltern.includes('?') ? '&' : '?' const adresse = 'https://tactictable.com/api/v1' + pfadMitFiltern + (cursor ? trenner + 'cursor=' + encodeURIComponent(cursor) : '') const antwort = await fetch(adresse, { headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' }, }) if (!antwort.ok) throw new Error('HTTP ' + antwort.status) const seiteDaten = (await antwort.json()) as Seite<T> alles.push(...seiteDaten.data) if (!seiteDaten.nextCursor) return alles cursor = seiteDaten.nextCursor } // Kein stilles Abbrechen: eine halbe Liste, die wie eine ganze aussieht, // ist der Fehler, den man erst im naechsten Quartal bemerkt. throw new Error('Mehr als ' + MAX_SEITEN + ' Seiten — Abfrage eingrenzen.')} // Aufruf: await alleSeiten('/reservations?dateFrom=2026-09-01&dateTo=2026-09-30&limit=200', token)import requests MAX_SEITEN = 1000 def geaenderte_gaeste(sitzung: requests.Session, seit: str) -> list: """Alle Gaeste, die sich seit `seit` (ISO-8601 mit Zone) geaendert haben.""" gesammelt = [] cursor = None for _ in range(MAX_SEITEN): parameter = {"limit": 200, "updatedSince": seit} if cursor: parameter["cursor"] = cursor antwort = sitzung.get("https://tactictable.com/api/v1/guests", params=parameter, timeout=30) antwort.raise_for_status() rumpf = antwort.json() gesammelt.extend(rumpf["data"]) cursor = rumpf["pagination"]["nextCursor"] if cursor is None: return gesammelt raise RuntimeError("Mehr als {} Seiten — Zeitfenster verkleinern.".format(MAX_SEITEN)) # Beim naechsten Lauf: den hoechsten updatedAt der letzten Runde uebergeben.# neue = geaenderte_gaeste(sitzung, "2026-09-14T00:00:00Z")# naechster_stand = max(g["updatedAt"] for g in neue) if neue else stand<?php function alle_gerichte(string $token): array{ $alles = []; $cursor = null; $seiten = 0; do { $adresse = 'https://tactictable.com/api/v1/menu-items?limit=200&available=true'; if ($cursor !== null) { $adresse .= '&cursor=' . rawurlencode($cursor); } $ch = curl_init($adresse); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token], CURLOPT_TIMEOUT => 30, ]); $rumpf = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException('HTTP ' . $status . ': ' . $rumpf); } $json = json_decode($rumpf, true, 512, JSON_THROW_ON_ERROR); $alles = array_merge($alles, $json['data']); $cursor = $json['nextCursor']; } while ($cursor !== null && ++$seiten < 1000); return $alles;}Siehe auch
- Ratenbegrenzung: wie viel, wovon, und was bei 429 zu tun istFünf Eimer, drei Kopfzeilen und ein Stundenkontingent für Gästedaten — mit fertigem Wiederholversuch in vier Sprachen.
- Reservierungen lesen, anlegen und ändernDas Reservierungsbuch: Liste mit zwanzig Filtern, Einzelabruf mit ETag, Anlegen durch dieselbe Torwache wie das Widget und Teiländerung ohne Datenverlust.
- Gästekartei lesen, anlegen und pflegenDie Stammdaten der Gäste: exakte Suche statt Auskunftei, drei Sichtbarkeitsstufen, Merkmale additiv ändern und der Newsletter-Zustand als eine einzige verlässliche Frage.