Delta sync

Většina partnerských integrací udržuje lokální zrcadlo zdrojů locco (zaměstnanci, vozidla, nákladová střediska, oddělení, cestovní záznamy, výplaty, odběry webhook). Po počátečním backfill je otázka na každém endpoint stejná: jak mohu provádět poll na to, co se změnilo od mé poslední synchronizace, aniž bych musel znovu číst každý řádek?

Odpovědí je query parameter modifiedSince. Je podporován na každém stránkovaném list endpoint a je to primární podporovaný způsob, jak provádět inkrementální synchronizaci vůči partnerskému API ve v1.

Ve zkratce

  • Každý stránkovaný list endpoint přijímá ?modifiedSince=<ISO 8601 UTC timestamp>.
  • Stránky jsou vraceny seřazené podle (updatedAt, id) ASC, když je modifiedSince nastaveno bez explicitního parametru sort. Toto je pořadí, které partneři potřebují k procházení časové osy změn vpřed bez opětovného čtení již zpracovaných řádků.
  • Velikost stránky je na straně serveru omezena na 100. Vyšší hodnoty jsou tiše oříznuty a pageSize v odpovědi odráží tuto oříznutou hodnotu (nikoli původní požadavek), takže aritmetika Math.ceil(total / pageSize) zůstává správná.
  • Cursor je updatedAt nejnovějšího řádku vráceného předchozím úspěšným poll. Uložte jej. Předejte jej při dalším poll. Opakujte.

Endpoints

Stejný kontrakt modifiedSince je podporován na:

ZdrojEndpoint
ZaměstnanciGET /api/v1/employees
VozidlaGET /api/v1/vehicles
Nákladová střediskaGET /api/v1/cost-centers
OdděleníGET /api/v1/departments
Cestovní záznamyGET /api/v1/travel-entries
VýplatyGET /api/v1/payouts
Odběry webhookGET /api/v1/webhooks

Doručování webhook (GET /api/v1/webhooks/{id}/deliveries) záměrně NEPODPORUJE modifiedSince. Doručení jsou pouze pro přidávání (append-only) a po uložení jsou efektivně neměnná, takže partneři, kteří chtějí sledovat stav doručení, provádějí poll podle id nebo filtru stavu.

Co vrací modifiedSince

Při poll jako:

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átí každý řádek, jehož updatedAt je rovno nebo větší než zadané časové razítko. Hodnota updatedAt je vypočítána na straně serveru jako hodnota editedAt daného řádku, přičemž u řádků, které nebyly nikdy upraveny, se použije createdAt, takže časové razítko je vždy vyplněno.

Řádek, který byl vytvořen v 2026-04-30T11:55:00Z a nikdy nebyl upraven, je vyloučen (jeho updatedAt s hodnotou 11:55:00Z je před cursor s hodnotou 12:00:00Z). Řádek, který byl vytvořen loni, ale upraven dnes, je zahrnut.

Záruka řazení

Když je modifiedSince nastaveno bez explicitního parametru sort, server seřadí stránku podle (updatedAt ASC, id ASC). To je klíčové pro správnost delta sync:

  • Bez id jako rozhodovacího kritéria (tiebreaker) by se dva řádky se stejným updatedAt objevily v nedeterministickém pořadí napříč požadavky a partneři procházející stránkování by tiše přeskočili nebo duplikovali řádky.
  • ASC (nejstarší změna jako první) znamená, že cursor partnera na konci stránky je nejvyšší updatedAt, které viděl, což je přirozený výchozí bod pro další poll.

Pokud předáte explicitní sort=...&sortDir=desc, server stále připojí id jako rozhodovací kritérium, takže stránkování zůstane stabilní, ale pořadí na stránce nebude takové, jaké byste pro delta sync chtěli. Pro případy použití synchronizace nechte sort nenastavené.

Omezení stránkování

Každý list endpoint omezuje pageSize na 100 na straně serveru. Pole pageSize v odpovědi odráží EFEKTIVNÍ oříznutou hodnotu, nikoli to, o co jste žádali, takže:

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

vrátí až 100 položek A tělo odpovědi, které říká "pageSize": 100. Odpověď také obsahuje pole totalPages vypočítané při EFEKTIVNÍ oříznuté velikosti stránky, takže nejjednodušší ukončovací podmínka smyčky je page >= totalPages. Vyhněte se přepočítávání stránek z total / pageSize vůči vaší původní hodnotě požadavku: vypočítali byste příliš málo stránek a tiše byste přišli o data.

page je podobně omezeno na minimum 1; nekladné hodnoty stránek jsou normalizovány. Pole page v odpovědi odráží skutečně vrácenou stránku.

Polling smyčka

Kontrakt je přímočarý: přečtěte si svůj uložený cursor, proveďte s ním poll, projděte každou stránku, uložte nejvyšší updatedAt z daného běhu jako další 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)

Několik detailů, které stojí za zmínku:

  • Uložte cursor až poté, co celý poll úspěšně skončí, včetně zápisu řádků do vašeho lokálního úložiště. Jinak při pádu uprostřed běhu ztratíte řádky, které jste stáhli, ale neuložili, a další poll je znovu nestáhne.
  • Cursor je exkluzivní na straně serveru, inkluzivní na vaší hranici. Řádek s updatedAt == cursor se objeví v dalším poll. Idempotentní upsert na vaší straně toto opětovné přečtení absorbuje. Pokud chcete striktní exkluzivitu, posuňte svůj uložený cursor na cursor + 1ms po úspěšném běhu.
  • Souběžné úpravy jsou eventuálně konzistentní. Řádek upravený uprostřed poll se může objevit na pozdější stránce (protože jeho updatedAt se posunulo za hranici stránky). Vaše logika upsert přepíše dřívější čtení tím novějším. Není potřeba žádné manuální řešení konfliktů.

Počáteční backfill

Při úplně prvním běhu předejte modifiedSince=null (nebo jej vynechte). Server vrátí každý řádek ve společnosti. S velikostí stránky 100 řádků představuje společnost s 5000 zaměstnanci 50 stránek. Přizpůsobte tempo polling podle rezervy rate-limit, kterou máte (viz Rate limits), a na konci uložte cursor.

Pro velmi rozsáhlé počáteční backfill partneři někdy preferují inicializaci pomocí modifiedSince=2000-01-01T00:00:00Z, aby využili stejnou cestu v kódu jako při ustáleném poll. Funkčně je to identické s vynecháním parametru.

Kombinace s webhook

Webhook (viz Webhooks) odesílají oznámení o událostech, když nastane doménová událost. Jsou doporučeným způsobem, jak reagovat na změny v téměř reálném čase. Jsou však best-effort: doručení může selhat (odběratel je nedostupný, výpadek sítě) a po vyčerpání politiky opakování je událost ztracena.

Delta sync je odolným protějškem. Běžný vzor:

  1. Webhook pro latenci. Reagujte na události během několika sekund od jejich spuštění.
  2. Hodinový delta sync jako záchranná síť. Zachyťte vše, co webhook zahodil (selhaná doručení, krátkodobě zakázaný odběr, výpadek partnera).

Obě rozhraní sdílejí stejný datový model. Payload webhook nese id zdrojů a partneři stahují celý zdroj přes GET /api/v1/{resource}/{id} pro získání aktuálního stavu. Pull endpoint je jediným zdrojem pravdy v obou tocích.