Continu synchroniseren
Use case stap voor stap om een kopie van een dossier actueel te houden met sync/bootstrap en sync/pull, de cursor te beheren en wijzigingen in batches te versturen met sync/push.
De NovaFisko-applicaties werken offline dankzij een wijzigingsstroom. Uw integratie kan diezelfde stroom gebruiken om een kopie van een dossier actueel te houden zonder alle lijsten opnieuw te lezen: een datawarehouse, een rapporteringstool of een ander boekhoudpakket.
Vereisten: een token met de bevoegdheid sync. Voeg read toe voor het eerste laden van de resources via hun gewone routes.
Het principe bestaat uit drie fasen:
- Bootstrap: u krijgt de inventaris van de resources en een startcursor.
- Eerste laadbeurt: u leest elke resource één keer.
- Pull: u vraagt regelmatig "wat is er nieuw sinds mijn cursor?".
Stap 1: bootstrap
curl https://api.novafisko.com/v1/companies/k3Jd9fPq2LmX/sync/bootstrap \
-H "Authorization: Bearer $NOVAFISKO_TOKEN" \
-H "X-Device-Id: bi-warehouse-prod" \
-H "X-Device-Name: Entrep%C3%B4t%20BI" \
-H "X-Device-Platform: integration"
{
"cursor": 48211,
"server_time": "2026-10-05T08:40:00+00:00",
"full_resync": false,
"retention_days": 90,
"resources": [
{"name": "company", "count": 1, "endpoint": "companies/k3Jd9fPq2LmX", "paginated": false},
{"name": "company_settings", "count": 1, "endpoint": "companies/k3Jd9fPq2LmX/settings", "paginated": false},
{"name": "third_parties", "count": 184, "endpoint": "companies/k3Jd9fPq2LmX/third-parties", "paginated": false},
{"name": "accounts", "count": 412, "endpoint": "companies/k3Jd9fPq2LmX/accounts", "paginated": false},
{"name": "entries", "count": 873, "endpoint": "companies/k3Jd9fPq2LmX/entries", "paginated": true},
{"name": "documents", "count": 640, "endpoint": "companies/k3Jd9fPq2LmX/documents", "paginated": true},
{"name": "bank_transactions", "count": 1290, "endpoint": "companies/k3Jd9fPq2LmX/bank-transactions", "paginated": true}
],
"company": {"id": 318, "public_token": "k3Jd9fPq2LmX", "name": "Le Comptoir Montois SRL", "version": 6},
"company_settings": {"vat": {"office_number": null}},
"user": {"id": 12, "name": "Claire Dumont", "role": "firm_admin", "role_rank": 90, "can_write": true, "can_force": true},
"push": {
"resources": {
"third_parties": {"ops": ["create", "update", "delete"], "actions": []},
"entries": {"ops": ["create", "action"], "actions": ["entry", "invoice", "reverse"]}
},
"max_mutations": 200,
"temp_id_prefix": "tmp-"
}
}
De lijsten zijn ingekort. Sla cursor op voordat u met de eerste laadbeurt begint: alles wat tijdens het laden verandert, wordt u bij de eerste pull opnieuw bezorgd.
Stap 2: eerste laadbeurt
Roep voor elke vermelding in resources het endpoint aan (relatief ten opzichte van /v1/). Wanneer paginated de waarde true heeft, doorloopt u alle pagina's zoals beschreven in Paginering. Wanneer de waarde false is, is het antwoord een volledige array of één enkel object.
def initial_load(session, base, bootstrap):
store = {}
for resource in bootstrap["resources"]:
url = f"{base}/{resource['endpoint']}"
if not resource["paginated"]:
store[resource["name"]] = session.get(url, timeout=60).json()
continue
rows, page = [], 1
while True:
payload = session.get(url, params={"per_page": 200, "page": page}, timeout=60).json()
rows.extend(payload["data"])
if page >= payload["last_page"]:
break
page += 1
store[resource["name"]] = rows
return store
Stap 3: incrementele pull
curl "https://api.novafisko.com/v1/companies/k3Jd9fPq2LmX/sync/pull?since=48211&limit=500" \
-H "Authorization: Bearer $NOVAFISKO_TOKEN" \
-H "X-Device-Id: bi-warehouse-prod"
| Parameter | Standaard | Beschrijving |
|---|---|---|
since |
Laatst ontvangen cursor | |
resources |
alle | Door komma's gescheiden lijst om alleen bepaalde resources te volgen: entries,third_parties |
limit |
500 |
Aantal wijzigingen per aanroep, maximaal 1000 |
{
"cursor": 48236,
"has_more": false,
"full_resync": false,
"changes": [
{
"resource": "third_parties",
"op": "upsert",
"id": 41,
"version": 4,
"data": {"id": 41, "type": "supplier", "code": "BRASSCOL", "name": "Brasserie des Collines SA", "email": "compta@brasserie-collines.example", "version": 4},
"changed_at": "2026-10-05T08:41:02+00:00",
"seq": 48230
},
{
"resource": "journal_rules",
"op": "delete",
"id": 9,
"version": 3,
"data": null,
"changed_at": "2026-10-05T08:41:30+00:00",
"seq": 48236
}
],
"server_time": "2026-10-05T08:41:39+00:00"
}
De wijzigingen toepassen
op |
Wat te doen |
|---|---|
upsert |
Het record id van de resource aanmaken of vervangen door data |
delete |
Het record verwijderen. Het is verwijderd of in de prullenbak geplaatst |
data heeft exact de vorm die de lijstroute van de resource teruggeeft. Elk record verschijnt maar één keer per antwoord, in zijn laatste toestand. Als u een version ontvangt die lager is dan of gelijk is aan de versie die u al hebt, negeer dan de wijziging.
Regels voor de cursor
- Pas alle wijzigingen van een antwoord toe en sla daarna de nieuwe
cursorop, indien mogelijk in dezelfde transactie. - Als
has_morede waardetrueheeft, roep dan onmiddellijk opnieuw aan met de nieuwe cursor. - Als
full_resyncde waardetrueheeft, is uw cursor onbruikbaar.changesis leeg: voer de eerste laadbeurt van stap 2 opnieuw uit en vertrek daarna van de teruggegevencursor.
full_resync treedt op wanneer since ontbreekt, wanneer hij ouder is dan de bewaartermijn van het logboek (90 dagen) of wanneer de server hem niet kent. Dat is met name het geval in het demokantoor na de nachtelijke reset.
Een wijziging wordt 2 seconden na het wegschrijven zichtbaar. Die vertraging garandeert dat een cursor nooit over een rij springt die nog wordt opgeslagen. Wees dus niet verbaasd als u een wijziging die u net hebt gemaakt niet onmiddellijk ziet.
Volledige lus
const BASE = "https://api.novafisko.com/v1";
const headers = {
Authorization: `Bearer ${process.env.NOVAFISKO_TOKEN}`,
Accept: "application/json",
"X-Device-Id": "bi-warehouse-prod",
};
async function syncOnce(company, store) {
let cursor = await store.getCursor(company);
for (;;) {
const url = `${BASE}/companies/${company}/sync/pull?since=${cursor ?? ""}&limit=1000`;
const page = await (await fetch(url, { headers })).json();
if (page.full_resync) {
await store.reloadEverything(company); // initial load again
await store.setCursor(company, page.cursor);
return;
}
await store.transaction(async (tx) => {
for (const change of page.changes) {
if (change.op === "delete") await tx.remove(change.resource, change.id);
else await tx.upsert(change.resource, change.id, change.version, change.data);
}
await tx.setCursor(company, page.cursor); // cursor saved with the data
});
cursor = page.cursor;
if (!page.has_more) return;
}
}
// Poll every 30 seconds; a webhook can trigger an immediate run
setInterval(() => syncOnce("k3Jd9fPq2LmX", store).catch(console.error), 30_000);
Een interval van 30 tot 60 seconden volstaat in de meeste gevallen. Om sneller te reageren zonder vaker te bevragen, start u een pull bij ontvangst van een webhook.
Stap 4: de status opvolgen
curl https://api.novafisko.com/v1/companies/k3Jd9fPq2LmX/sync/status \
-H "Authorization: Bearer $NOVAFISKO_TOKEN" \
-H "X-Device-Id: bi-warehouse-prod"
{
"cursor": 48236,
"server_time": "2026-10-05T08:45:00+00:00",
"pending_conflicts": 0,
"last_push_at": "2026-10-05T07:58:12+00:00",
"devices": [
{"id": "bi-warehouse-prod", "name": "Entrepôt BI", "platform": "integration", "user": {"id": 12, "name": "Claire Dumont"}, "last_seen_at": "2026-10-05T08:45:00+00:00", "last_push_at": null, "last_cursor": 48236}
]
}
Vergelijk uw cursor met cursor om uw achterstand te meten. Uw connector verschijnt in devices dankzij de header X-Device-Id.
Wijzigingen in batches versturen
POST sync/push past tot 200 mutaties toe in één aanroep, in volgorde, elk in haar eigen transactie. Elke mutatie wordt opnieuw afgespeeld door de overeenkomstige REST-actie, met dezelfde validaties.
curl -X POST https://api.novafisko.com/v1/companies/k3Jd9fPq2LmX/sync/push \
-H "Authorization: Bearer $NOVAFISKO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"device": {"id": "erp-atelier-prod", "name": "ERP Atelier", "platform": "integration"},
"mutations": [
{
"client_mutation_id": "c1b9a8d2-51b5-4a61-8f52-3a2d1d8c5a01",
"occurred_at": "2026-10-05T08:00:00Z",
"resource": "third_parties",
"op": "create",
"client_temp_id": "tmp-7d1f",
"data": {"type": "supplier", "name": "Maraîchers du Borinage SC", "vat_number": "BE0999900233"}
},
{
"client_mutation_id": "c1b9a8d2-51b5-4a61-8f52-3a2d1d8c5a02",
"occurred_at": "2026-10-05T08:00:05Z",
"resource": "entries",
"op": "action",
"action": "invoice",
"data": {
"journal_id": 3,
"third_party_id": "tmp-7d1f",
"entry_date": "2026-10-03",
"reference": "2026-441",
"lines": [{"account": "604000", "amount": "318.40", "vat_code": "A06"}]
}
}
]
}'
De tweede mutatie verwijst naar de derde die door de eerste is aangemaakt, dankzij de tijdelijke identificator tmp-7d1f. De server vervangt hem door de echte id, zowel in dezelfde batch als in de volgende.
{
"results": [
{
"client_mutation_id": "c1b9a8d2-51b5-4a61-8f52-3a2d1d8c5a01",
"status": "applied",
"resource": "third_parties",
"op": "create",
"id": 215,
"client_temp_id": "tmp-7d1f",
"version": 1,
"data": {"id": 215, "type": "supplier", "name": "Maraîchers du Borinage SC", "version": 1},
"conflict": null,
"retryable": false
},
{
"client_mutation_id": "c1b9a8d2-51b5-4a61-8f52-3a2d1d8c5a02",
"status": "applied",
"resource": "entries",
"op": "action",
"action": "invoice",
"id": 1851,
"data": {"id": 1851, "number": 213, "status": "posted"},
"conflict": null,
"retryable": false
}
],
"cursor": 48251,
"server_time": "2026-10-05T08:46:10+00:00"
}
status |
Betekenis | Wat te doen |
|---|---|---|
applied |
Mutatie toegepast | Uw kopie vervangen door data |
duplicate |
client_mutation_id al verwerkt, oorspronkelijk resultaat teruggegeven |
Niets, de mutatie is verworven |
conflict |
Verwerkt volgens de prioriteitsregels | Uw kopie vervangen door data, de mutatie uit uw wachtrij halen |
rejected |
Geweigerd, zie code |
Definitief, tenzij retryable de waarde true heeft |
Alleen bepaalde bewerkingen verlopen via sync/push. De exacte lijst staat in push.resources van de bootstrap. Al de rest komt terug als rejected met de code not_supported_offline: gebruik dan de klassieke REST-route.
Wanneer twee wijzigingen betrekking hebben op hetzelfde record, voegt NovaFisko ze veld per veld samen. Gevalideerde of vergrendelde boekhoudgegevens winnen altijd. De volledige regels staan in Conflicten en prioriteiten.
Beperkingen om te kennen
- De massaverwerkingen (afpunting, vergrendeling van de periodes bij de afsluiting, terugzetten naar te boeken na een tegenboeking) leveren geen rij op in de stroom. Laad na een tegenboeking
documentsenbank_transactionsopnieuw. - Het wijzigingslogboek wordt 90 dagen bewaard. Een connector die langer stilligt, moet een volledige laadbeurt opnieuw uitvoeren.
- De stroom is eigen aan één dossier. Om een heel kantoor te volgen, start u één lus per dossier.