Aller au contenu
Documentation
Français
Ouvrir l'application

Synchroniser en continu

Cas d'usage pas à pas pour tenir une copie d'un dossier à jour avec sync/bootstrap et sync/pull, gérer le curseur et envoyer des modifications par lots avec sync/push.

Les applications NovaFisko fonctionnent hors ligne grâce à un flux de changements. Votre intégration peut utiliser ce même flux pour tenir une copie d'un dossier à jour sans relire toutes les listes : un entrepôt de données, un outil de reporting ou un autre logiciel comptable.

Prérequis : un jeton avec la capacité sync. Pour le premier chargement des ressources par leurs routes habituelles, ajoutez read.

Le principe tient en trois temps :

  1. Bootstrap : vous obtenez l'inventaire des ressources et un curseur de départ.
  2. Chargement initial : vous lisez chaque ressource une fois.
  3. Pull : vous demandez régulièrement « quoi de neuf depuis mon curseur ? ».

Étape 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-"
  }
}

Les listes sont abrégées. Enregistrez cursor avant de commencer le chargement initial : tout ce qui change pendant le chargement vous sera rejoué au premier pull.

Étape 2 : chargement initial

Pour chaque entrée de resources, appelez endpoint (relatif à /v1/). Quand paginated vaut true, parcourez toutes les pages comme décrit dans Pagination. Quand il vaut false, la réponse est un tableau complet ou un objet unique.

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

Étape 3 : tirage incrémental

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"
Paramètre Défaut Description
since Dernier curseur reçu
resources toutes Liste séparée par des virgules pour ne suivre que certaines ressources : entries,third_parties
limit 500 Nombre de changements par appel, 1000 au maximum
{
  "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"
}

Appliquer les changements

op À faire
upsert Créer ou remplacer l'enregistrement id de la ressource par data
delete Retirer l'enregistrement. Il a été supprimé ou placé dans la corbeille

data a exactement la forme renvoyée par la route de liste de la ressource. Chaque enregistrement n'apparaît qu'une fois par réponse, dans son dernier état. Si vous recevez une version inférieure ou égale à celle que vous détenez, ignorez le changement.

Règles du curseur

  1. Appliquez tous les changements d'une réponse, puis enregistrez le nouveau cursor, dans la même transaction si possible.
  2. Si has_more vaut true, rappelez immédiatement avec le nouveau curseur.
  3. Si full_resync vaut true, votre curseur est inutilisable. changes est vide : refaites le chargement initial de l'étape 2, puis repartez du cursor renvoyé.

full_resync survient quand since est absent, quand il est plus ancien que la rétention du journal (90 jours) ou quand il est inconnu du serveur. C'est notamment le cas dans le cabinet de démonstration après la réinitialisation nocturne.

Note

Un changement devient visible 2 secondes après son écriture. Ce délai garantit qu'un curseur ne saute jamais par-dessus une ligne encore en cours d'enregistrement. Ne vous étonnez donc pas de ne pas voir immédiatement une modification que vous venez de faire.

Boucle complète

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

Un intervalle de 30 à 60 secondes suffit dans la plupart des cas. Pour réagir plus vite sans interroger plus souvent, déclenchez un pull à la réception d'un webhook.

Étape 4 : surveiller l'état

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

Comparez votre curseur à cursor pour mesurer votre retard. Votre connecteur apparaît dans devices grâce à l'en-tête X-Device-Id.

Envoyer des modifications par lots

POST sync/push applique jusqu'à 200 mutations en un appel, dans l'ordre, chacune dans sa propre transaction. Chaque mutation est rejouée par l'action REST correspondante, avec les mêmes validations.

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

La seconde mutation référence le tiers créé par la première grâce à l'identifiant temporaire tmp-7d1f. Le serveur le remplace par l'id réel, dans le même lot comme dans les suivants.

{
  "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 Signification À faire
applied Mutation appliquée Remplacer votre copie par data
duplicate client_mutation_id déjà traité, résultat d'origine renvoyé Rien, la mutation est acquise
conflict Traité selon les règles de priorité Remplacer votre copie par data, retirer la mutation de votre file
rejected Refusé, voir code Définitif, sauf si retryable vaut true
Attention

Seules certaines opérations passent par sync/push. La liste exacte figure dans push.resources du bootstrap. Tout le reste revient rejected avec le code not_supported_offline : utilisez alors la route REST classique.

Quand deux modifications portent sur le même enregistrement, NovaFisko fusionne champ par champ. Les données comptables validées ou verrouillées gagnent toujours. Les règles complètes figurent dans Conflits et priorités.

Limites à connaître

  • Les traitements en masse (lettrage, verrouillage des périodes à la clôture, remise à comptabiliser après une extourne) ne produisent pas de ligne dans le flux. Après une extourne, rechargez documents et bank_transactions.
  • Le journal des changements est conservé 90 jours. Un connecteur arrêté plus longtemps devra refaire un chargement complet.
  • Le flux est propre à un dossier. Pour suivre tout un cabinet, lancez une boucle par dossier.

Voir aussi