Delta sync

Väčšina partnerských integrácií udržiava lokálne zrkadlo zdrojov locco (zamestnanci, vozidlá, nákladové strediská, oddelenia, cestovné záznamy, výplaty, odbery pre webhook). Po počiatočnom backfill je otázka na každom endpoint rovnaká: ako môžem dopytovať (poll) to, čo sa zmenilo od mojej poslednej synchronizácie bez toho, aby som musel znova čítať každý riadok?

Odpoveďou je query parameter modifiedSince. Je podporovaný na každom stránkovanom list endpoint a je to primárny podporovaný spôsob, ako vykonávať inkrementálnu synchronizáciu voči partnerskému API vo v1.

V skratke

  • Každý stránkovaný list endpoint akceptuje ?modifiedSince=<ISO 8601 UTC timestamp>.
  • Stránky sa vracajú zoradené podľa (updatedAt, id) ASC, keď je modifiedSince nastavené bez explicitného parametra sort. Toto je poradie, ktoré partneri potrebujú na to, aby mohli prechádzať časovou osou zmien vpred bez opätovného čítania už spracovaných riadkov.
  • Veľkosť stránky je na strane servera obmedzená na 100. Vyššie hodnoty sú potichu orezané a pageSize v odpovedi odráža túto orezanú hodnotu (nie pôvodnú požiadavku), takže aritmetika Math.ceil(total / pageSize) zostáva správna.
  • cursor je updatedAt najnovšieho riadka vráteného predchádzajúcim úspešným dopytom (poll). Uložte ho. Pošlite ho pri ďalšom poll. Opakujte.

Endpoints

Rovnaký kontrakt pre modifiedSince je podporovaný na:

ResourceEndpoint
ZamestnanciGET /api/v1/employees
VozidláGET /api/v1/vehicles
Nákladové strediskáGET /api/v1/cost-centers
OddeleniaGET /api/v1/departments
Cestovné záznamyGET /api/v1/travel-entries
VýplatyGET /api/v1/payouts
Odbery pre webhookGET /api/v1/webhooks

Doručenia pre webhook (GET /api/v1/webhooks/{id}/deliveries) zámerne NEPODPORUJÚ modifiedSince. Doručenia sú iba na pridávanie (append-only) a po uložení sú v podstate nemenné, takže partneri, ktorí chcú sledovať stav doručenia, namiesto toho dopytujú (poll) podľa id alebo podľa filtra stavu.

Čo vracia modifiedSince

Pri dopyte (poll) ako:

http
GET /api/v1/employees?modifiedSince=2026-04-30T12:00:00Z
Authorization: Bearer locco_live_...
X-Company-Id: 9c4e0d70-9d59-4a4a-a7d9-1e5ff6f1e3ec

Server vráti každý riadok, ktorého updatedAt je rovnaký alebo novší ako poskytnutá časová pečiatka. updatedAt sa počíta na strane servera ako hodnota editedAt daného riadka, pričom pre riadky, ktoré neboli nikdy upravené, sa použije createdAt, takže časová pečiatka je vždy vyplnená.

Riadok, ktorý bol vytvorený o 2026-04-30T11:55:00Z a nikdy nebol upravený, je vylúčený (jeho updatedAt s hodnotou 11:55:00Z je pred cursor s hodnotou 12:00:00Z). Riadok, ktorý bol vytvorený minulý rok, ale upravený dnes, je zahrnutý.

Záruka triedenia

Keď je modifiedSince nastavené bez explicitného parametra sort, server zoradí stránku podľa (updatedAt ASC, id ASC). Toto je kľúčové pre správnosť delta sync:

  • Bez id ako rozhodovacieho kritéria (tiebreaker) by sa dva riadky s identickým updatedAt zobrazovali v nedeterministickom poradí naprieč požiadavkami a partneri prechádzajúci stránkovaním by potichu preskočili alebo duplikovali riadky.
  • ASC (najstaršia zmena ako prvá) znamená, že partnerov cursor na konci stránky je najvyššie updatedAt, aké doteraz videli, čo je prirodzený počiatočný bod pre ďalší poll.

Ak pošlete explicitné sort=...&sortDir=desc, server stále pripojí id ako rozhodovacie kritérium, takže stránkovanie zostane stabilné, ale poradie na stránke nebude také, aké požadujete pre delta sync. Pre prípady použitia synchronizácie nechajte sort nenastavené.

Orezanie stránkovania

Každý list endpoint obmedzuje (clamps) pageSize na 100 na strane servera. Pole pageSize v odpovedi odráža EFEKTÍVNU orezanú hodnotu, nie to, o čo ste žiadali, takže:

http
GET /api/v1/employees?pageSize=10000

vráti maximálne 100 položiek A telo odpovede, ktoré hovorí "pageSize": 100. Odpoveď tiež obsahuje pole totalPages vypočítané pri EFEKTÍVNEJ orezanej veľkosti stránky, takže najjednoduchší ukončovač slučky je page >= totalPages. Vyhnite sa prepočítavaniu stránok z total / pageSize voči vašej pôvodnej hodnote požiadavky: vypočítate príliš málo stránok a potichu prídete o dáta.

page je podobne orezané na minimum 1; nekladné hodnoty stránok sú normalizované. Pole page v odpovedi odráža skutočnú vrátenú stránku.

Slučka dopytovania

Kontrakt je priamočiary: prečítajte si váš uložený cursor, vykonajte s ním poll, prejdite každú stránku, uložte najvyššie updatedAt z tohto behu ako ďalší cursor.

python
import time
import requests
API = "https://api.getlocco.com"
HEADERS = {
"Authorization": "Bearer locco_live_<your-key>",
"X-Company-Id": "<your-company-id>",
}
def poll_employees(cursor: str | None) -> tuple[list[dict], str]:
"""
Walk every page of employees changed since `cursor`. Returns the rows
plus the new cursor (the highest updatedAt seen during this run).
"""
rows = []
page = 1
new_cursor = cursor
while True:
params = {"page": page, "pageSize": 100}
if cursor is not None:
params["modifiedSince"] = cursor
r = requests.get(f"{API}/api/v1/employees", headers=HEADERS, params=params)
r.raise_for_status()
body = r.json()
rows.extend(body["items"])
# Advance the cursor to the highest updatedAt on this page.
for row in body["items"]:
updated_at = row.get("updatedAt") or row.get("createdAt")
if updated_at and (new_cursor is None or updated_at > new_cursor):
new_cursor = updated_at
# Use the response's totalPages, NOT a value computed from the request,
# so the loop terminates correctly even if the server clamped pageSize.
if page >= body["totalPages"]:
break
page += 1
return rows, new_cursor
# First run: cursor=None pulls everything (full backfill).
rows, cursor = poll_employees(cursor=None)
save_to_local_db(rows)
save_cursor(cursor)
# Steady state: poll every N minutes with the saved cursor.
while True:
time.sleep(300) # 5 minutes
cursor = load_cursor()
rows, new_cursor = poll_employees(cursor=cursor)
save_to_local_db(rows)
save_cursor(new_cursor)

Niekoľko detailov, na ktoré stojí za to upozorniť:

  • Uložte cursor až po úspešnom dokončení celého poll, vrátane zápisu riadkov do vášho lokálneho úložiska. V opačnom prípade pri páde uprostred behu stratíte riadky, ktoré ste stiahli, ale neuložili, a ďalší poll ich už znova nestiahne.
  • cursor je exkluzívny na strane servera, inkluzívny na vašej hranici. Riadok s updatedAt == cursor sa objaví v ďalšom poll. Idempotentné upsert operácie na vašej strane absorbujú opätovné prečítanie. Ak chcete striktnú exkluzivitu, posuňte váš uložený cursor na cursor + 1ms po úspešnom behu.
  • Súbežné úpravy sú eventuálne konzistentné. Riadok upravený uprostred poll sa môže objaviť na neskoršej stránke (pretože jeho updatedAt sa posunulo za hranicu stránky). Vaša upsert logika prepíše skoršie prečítanie tým novším. Nie je potrebné žiadne manuálne riešenie konfliktov.

Počiatočný backfill

Pri úplne prvom behu pošlite modifiedSince=null (alebo ho vynechajte). Server vráti každý riadok v spoločnosti. Pri veľkosti stránky 100 riadkov predstavuje spoločnosť s 5000 zamestnancami 50 stránok. Prispôsobte tempo dopytovania rezerve pre rate-limit, ktorú máte (pozrite si Rate limits), a na konci uložte cursor.

Pre veľmi veľké počiatočné backfill partneri niekedy uprednostňujú inicializáciu s modifiedSince=2000-01-01T00:00:00Z, aby použili rovnakú cestu v kóde ako pri bežnom poll v ustálenom stave. Funkčne je to identické s vynechaním parametra.

Kombinácia s webhooks

Webhooks (pozrite si Webhooks) odosielajú notifikácie o udalostiach, keď nastane doménová udalosť. Sú odporúčaným spôsobom, ako reagovať na zmeny v takmer reálnom čase. Sú však best-effort: doručenie môže zlyhať (odberateľ je nedostupný, výpadok siete) a po vyčerpaní politiky opakovaných pokusov je udalosť stratená.

Delta sync je odolným náprotivkom. Bežný vzor:

  1. Webhooks pre latenciu. Reagujte na udalosti v priebehu niekoľkých sekúnd od ich vzniku.
  2. Hodinový delta sync ako záchranná sieť. Zachyťte všetko, čo webhooks zahodili (zlyhané doručenia, krátkodobo zakázaný odber, výpadok partnera).

Tieto dve rozhrania zdieľajú rovnaký dátový model. Payload pre webhook nesie id zdrojov a partneri sťahujú celý zdroj cez GET /api/v1/{resource}/{id} pre získanie aktuálneho stavu. Tento pull endpoint je jediným zdrojom pravdy v oboch tokoch.