Delta sinkronizacija

Većina partnerskih integracija drži lokalni odraz locco resursa (zaposlenici, vozila, mjesta troška, odjeli, putni unosi, isplate, webhook pretplate). Nakon početnog punjenja, pitanje je isto na svakom endpointu: kako anketirati što se promijenilo od posljednje sinkronizacije bez ponovnog čitanja svakog reda?

Odgovor je modifiedSince query parametar. Podržan je na svakom paginiranom list endpointu i primarni je podržani način inkrementalne sinkronizacije s partner API-jem u v1.

U kratkim crtama

  • Svaki paginirani list endpoint prihvaća ?modifiedSince=<ISO 8601 UTC vremenska oznaka>.
  • Stranice se vraćaju sortirane (updatedAt, id) ASC kada je modifiedSince postavljen bez eksplicitnog sort parametra. To je redoslijed koji partneri trebaju kako bi prošli kroz vremensku liniju promjena bez ponovnog čitanja već obrađenih redova.
  • Veličina stranice ograničena je na 100 na strani servera. Veće vrijednosti se tiho ograničavaju, a pageSize u odgovoru odražava ograničenu vrijednost (a ne sirovi zahtjev) tako da Math.ceil(total / pageSize) aritmetika ostaje ispravna.
  • Kursor je updatedAt najnovijeg reda vraćenog prethodnom uspješnom anketiranju. Pohranite ga. Proslijedite ga u sljedećem anketiranju. Ponovite.

Endpointi

Isti modifiedSince ugovor podržan je na:

ResursEndpoint
ZaposleniciGET /api/v1/employees
VozilaGET /api/v1/vehicles
Mjesta troškaGET /api/v1/cost-centers
OdjeliGET /api/v1/departments
Putni unosiGET /api/v1/travel-entries
IsplateGET /api/v1/payouts
Webhook pretplateGET /api/v1/webhooks

Webhook isporuke (GET /api/v1/webhooks/{id}/deliveries) namjerno NE podržavaju modifiedSince. Isporuke se samo dodaju i efektivno su nepromjenjive nakon pohrane, pa partneri koji žele pratiti stanje isporuka ankete po id-u ili po filtru statusa.

Što modifiedSince vraća

Za anketu poput:

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

Server vraća svaki red čiji updatedAt je u trenutku ili nakon proslijeđene vremenske oznake. updatedAt se računa na strani servera kao vrijednost editedAt reda, s povratkom na createdAt za redove koji nikada nisu uređeni, tako da je vremenska oznaka uvijek popunjena.

Red koji je kreiran u 2026-04-30T11:55:00Z i nikada nije uređen je isključen (njegov updatedAt od 11:55:00Z je prije kursora od 12:00:00Z). Red koji je kreiran prošle godine, ali uređen danas, je uključen.

Jamstvo sortiranja

Kada je modifiedSince postavljen bez eksplicitnog sort parametra, server sortira stranicu po (updatedAt ASC, id ASC). Ovo je ključno za ispravnost delta sinkronizacije:

  • Bez id razrješivača izjednačenih vrijednosti, dva reda s identičnim updatedAt pojavila bi se nedeterministički kroz zahtjeve, a partneri koji prolaze kroz paginaciju tiho bi preskakali ili duplicirali redove.
  • ASC (najstarija promjena prva) znači da je partnerov kursor na kraju stranice najveći updatedAt koji je vidio, što je prirodna polazna točka za sljedeću anketu.

Ako proslijedite eksplicitni sort=...&sortDir=desc, server i dalje dodaje id razrješivač tako da paginacija ostaje stabilna, ali redoslijed stranica neće biti onaj koji želite za delta sinkronizaciju. Za sinkronizacijske slučajeve upotrebe, ostavite sort nepostavljenim.

Ograničenje paginacije

Svaki list endpoint ograničava pageSize na 100 na strani servera. Polje pageSize u odgovoru odražava EFEKTIVNU ograničenu vrijednost, a ne ono što ste tražili, pa:

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

vraća do 100 stavki I tijelo odgovora koje kaže "pageSize": 100. Odgovor također nosi polje totalPages izračunato pri EFEKTIVNOJ ograničenoj veličini stranice, pa je najjednostavniji terminator petlje page >= totalPages. Izbjegavajte ponovno računanje broja stranica iz total / pageSize prema vašoj sirovoj vrijednosti zahtjeva: izračunat ćete premalo stranica i tiho propustiti podatke.

page je slično ograničen na minimum 1; nepozitivne vrijednosti stranice se normaliziraju. Polje page u odgovoru odražava stvarno vraćenu stranicu.

Petlja anketiranja

Ugovor je jednostavan: pročitajte pohranjeni kursor, anketirajte s njim, prođite svaku stranicu, spremite najveći updatedAt iz prolaza kao sljedeći kursor.

python
import time
import requests
API = "https://api.locco.hr"
HEADERS = {
"Authorization": "Bearer locco_live_<your-key>",
"X-Company-Id": "<your-company-id>",
}
def poll_employees(cursor: str | None) -> tuple[list[dict], str]:
"""
Prođi kroz svaku stranicu zaposlenika promijenjenih od `cursor`.
Vraća redove plus novi kursor (najveći updatedAt viđen tijekom prolaza).
"""
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"])
# Pomakni kursor na najveći updatedAt na ovoj stranici.
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
# Koristi totalPages iz odgovora, NE vrijednost izračunatu iz zahtjeva,
# tako da petlja ispravno završava čak i ako je server ograničio pageSize.
if page >= body["totalPages"]:
break
page += 1
return rows, new_cursor
# Prvo izvršavanje: cursor=None povlači sve (potpuno punjenje).
rows, cursor = poll_employees(cursor=None)
save_to_local_db(rows)
save_cursor(cursor)
# Stalno stanje: anketiraj svakih N minuta s pohranjenim kursorom.
while True:
time.sleep(300) # 5 minuta
cursor = load_cursor()
rows, new_cursor = poll_employees(cursor=cursor)
save_to_local_db(rows)
save_cursor(new_cursor)

Nekoliko suptilnosti vrijedno spomenuti:

  • Spremite kursor tek nakon što cijela anketa uspije, uključujući zapisivanje redova u vašu lokalnu pohranu. U suprotnom pad usred izvršavanja gubi redove koje ste dohvatili, ali niste pohranili, a sljedeća anketa ih neće ponovno dohvatiti.
  • Kursor je ekskluzivan na strani servera, inkluzivan na vašoj granici. Red s updatedAt == cursor pojavit će se u sljedećem anketiranju. Idempotentni upserti na vašoj strani apsorbiraju ponovno čitanje. Ako želite strogu ekskluzivnost, pomaknite svoj pohranjeni kursor na cursor + 1ms nakon uspješnog prolaza.
  • Istovremene izmjene su eventualno konzistentne. Red uređen usred ankete mogao bi se pojaviti na kasnijoj stranici (jer je njegov updatedAt napredovao izvan granice stranice). Vaša upsert logika će prebrisati raniji prikaz s novijim. Nije potrebno ručno rješavanje sukoba.

Početno punjenje

Pri prvom izvršavanju, proslijedite modifiedSince=null (ili izostavite). Server vraća svaki red u tvrtki. S veličinom stranice od 100 redova, tvrtka s 5000 zaposlenika ima 50 stranica. Prilagodite ritam anketiranja prema rezervi ograničenja zahtjeva koju imate (vidi Ograničenja zahtjeva) i pohranite kursor na kraju.

Za vrlo velika početna punjenja, partneri ponekad preferiraju početnu vrijednost modifiedSince=2000-01-01T00:00:00Z kako bi koristili istu putanju koda kao i anketa stalnog stanja. Funkcionalno identično izostavljanju parametra.

Kombiniranje s webhooksima

Webhooks (vidi Webhooks) gura obavijesti o događajima kada se okine domenski događaj. Preporučeni su način reagiranja na promjene u realnom vremenu. Ali su “best-effort”: isporuka može propasti (pretplatnik nedostupan, mrežni problem) i nakon iscrpljivanja politike ponovnih pokušaja, događaj je nestao.

Delta sinkronizacija je trajni pandan. Uobičajeni obrazac:

  1. Webhooks za latenciju. Reagirajte na događaje u sekundama nakon okidanja.
  2. Satna delta sinkronizacija kao sigurnosna mreža. Uhvatite što god su webhooks ispustili (neuspjele isporuke, pretplata kratko onemogućena, prekid rada na strani partnera).

Dvije površine dijele isti podatkovni model. Webhook tijela nose id-ove resursa, a partneri povlače cijeli resurs preko GET /api/v1/{resource}/{id} za trenutno stanje. Pull endpoint je jedinstveni izvor istine u oba toka.