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 :
- Bootstrap : vous obtenez l'inventaire des ressources et un curseur de départ.
- Chargement initial : vous lisez chaque ressource une fois.
- 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
- Appliquez tous les changements d'une réponse, puis enregistrez le nouveau
cursor, dans la même transaction si possible. - Si
has_morevauttrue, rappelez immédiatement avec le nouveau curseur. - Si
full_resyncvauttrue, votre curseur est inutilisable.changesest vide : refaites le chargement initial de l'étape 2, puis repartez ducursorrenvoyé.
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.
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);
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 |
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
documentsetbank_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.