Zum Inhalt springen
Dokumentation
Deutsch
App öffnen

Fortlaufend synchronisieren

Anwendungsfall Schritt für Schritt, um die Kopie eines Mandats mit sync/bootstrap und sync/pull aktuell zu halten, den Cursor zu verwalten und Änderungen stapelweise mit sync/push zu senden.

Die NovaFisko-Anwendungen funktionieren offline dank eines Änderungsstreams. Ihre Integration kann denselben Stream nutzen, um die Kopie eines Mandats aktuell zu halten, ohne alle Listen neu zu lesen: ein Data Warehouse, ein Reporting-Werkzeug oder eine andere Buchhaltungssoftware.

Voraussetzungen: ein Token mit der Berechtigung sync. Für das erste Laden der Ressourcen über ihre üblichen Routen fügen Sie read hinzu.

Das Prinzip besteht aus drei Phasen:

  1. Bootstrap: Sie erhalten das Inventar der Ressourcen und einen Startcursor.
  2. Erstladen: Sie lesen jede Ressource einmal.
  3. Pull: Sie fragen regelmäßig „Was gibt es Neues seit meinem Cursor?“.

Schritt 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-"
  }
}

Die Listen sind gekürzt. Speichern Sie cursor, bevor Sie mit dem Erstladen beginnen: Alles, was sich während des Ladens ändert, wird Ihnen beim ersten pull erneut geliefert.

Schritt 2: Erstladen

Rufen Sie für jeden Eintrag von resources den endpoint auf (relativ zu /v1/). Hat paginated den Wert true, durchlaufen Sie alle Seiten wie unter Paginierung beschrieben. Hat es den Wert false, ist die Antwort ein vollständiges Array oder ein einzelnes Objekt.

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

Schritt 3: inkrementeller Abruf

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 Standard Beschreibung
since Zuletzt erhaltener Cursor
resources alle Kommagetrennte Liste, um nur bestimmte Ressourcen zu verfolgen: entries,third_parties
limit 500 Anzahl der Änderungen pro Aufruf, höchstens 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"
}

Die Änderungen anwenden

op Was zu tun ist
upsert Den Datensatz id der Ressource anlegen oder durch data ersetzen
delete Den Datensatz entfernen. Er wurde gelöscht oder in den Papierkorb verschoben

data hat genau die Form, die die Listenroute der Ressource liefert. Jeder Datensatz erscheint nur einmal pro Antwort, in seinem letzten Zustand. Wenn Sie eine version erhalten, die kleiner oder gleich der bei Ihnen vorhandenen ist, ignorieren Sie die Änderung.

Regeln für den Cursor

  1. Wenden Sie alle Änderungen einer Antwort an und speichern Sie danach den neuen cursor, möglichst in derselben Transaktion.
  2. Hat has_more den Wert true, rufen Sie sofort mit dem neuen Cursor erneut auf.
  3. Hat full_resync den Wert true, ist Ihr Cursor unbrauchbar. changes ist leer: Führen Sie das Erstladen aus Schritt 2 erneut durch und setzen Sie dann beim zurückgegebenen cursor neu an.

full_resync tritt auf, wenn since fehlt, wenn es älter ist als die Aufbewahrungsdauer des Änderungsprotokolls (90 Tage) oder wenn es dem Server unbekannt ist. Das ist insbesondere in der Demo-Kanzlei nach dem nächtlichen Zurücksetzen der Fall.

Hinweis

Eine Änderung wird 2 Sekunden nach ihrem Schreiben sichtbar. Diese Verzögerung stellt sicher, dass ein Cursor niemals eine Zeile überspringt, die gerade noch gespeichert wird. Wundern Sie sich also nicht, wenn Sie eine soeben vorgenommene Änderung nicht sofort sehen.

Vollständige Schleife

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);
Tipp

Ein Intervall von 30 bis 60 Sekunden genügt in den meisten Fällen. Um schneller zu reagieren, ohne häufiger abzufragen, lösen Sie beim Empfang eines Webhooks einen pull aus.

Schritt 4: den Zustand überwachen

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}
  ]
}

Vergleichen Sie Ihren Cursor mit cursor, um Ihren Rückstand zu messen. Ihr Konnektor erscheint dank des Headers X-Device-Id in devices.

Änderungen stapelweise senden

POST sync/push wendet bis zu 200 Mutationen in einem Aufruf an, der Reihe nach, jede in ihrer eigenen Transaktion. Jede Mutation wird über die entsprechende REST-Aktion ausgeführt, mit denselben Validierungen.

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"}]
        }
      }
    ]
  }'

Die zweite Mutation verweist über die temporäre Kennung tmp-7d1f auf den Geschäftspartner, den die erste angelegt hat. Der Server ersetzt sie durch die tatsächliche id, im selben Stapel wie in den folgenden.

{
  "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 Bedeutung Was zu tun ist
applied Mutation angewendet Ihre Kopie durch data ersetzen
duplicate client_mutation_id bereits verarbeitet, ursprüngliches Ergebnis zurückgegeben Nichts, die Mutation ist übernommen
conflict Nach den Prioritätsregeln verarbeitet Ihre Kopie durch data ersetzen, die Mutation aus Ihrer Warteschlange entfernen
rejected Abgelehnt, siehe code Endgültig, außer wenn retryable den Wert true hat
Achtung

Nur bestimmte Vorgänge laufen über sync/push. Die genaue Liste steht in push.resources des Bootstraps. Alles andere kommt als rejected mit dem Code not_supported_offline zurück: Verwenden Sie dann die klassische REST-Route.

Betreffen zwei Änderungen denselben Datensatz, führt NovaFisko sie Feld für Feld zusammen. Gebuchte oder gesperrte Buchhaltungsdaten haben immer Vorrang. Die vollständigen Regeln finden Sie unter Konflikte und Prioritäten.

Grenzen, die Sie kennen sollten

  • Massenverarbeitungen (Ausziffern, Sperren der Perioden beim Abschluss, Zurücksetzen auf „zu buchen“ nach einer Stornobuchung) erzeugen keine Zeile im Stream. Laden Sie nach einer Stornobuchung documents und bank_transactions neu.
  • Das Änderungsprotokoll wird 90 Tage aufbewahrt. Ein Konnektor, der länger stillstand, muss ein vollständiges Laden wiederholen.
  • Der Stream gehört zu einem einzelnen Mandat. Um eine ganze Kanzlei zu verfolgen, starten Sie eine Schleife pro Mandat.

Siehe auch