Aller au contenu principal

Opérations bancaires

Une opération bancaire (bank_operation) est une ligne d'un compte bancaire : un mouvement, avec son montant, sa date, le libellé de la banque, ce à quoi il a été rapproché et l'écriture comptable qu'il a produite. C'est la ressource qui porte la vie d'un mouvement, de son arrivée jusqu'à sa comptabilisation.

Une opération arrive d'une synchronisation (Synchronisations bancaires) ou de l'extraction d'un relevé importé (Relevés bancaires) - dès l'extraction, avant toute validation : valider le relevé comptabilise ses lignes, cela ne les crée pas. Elle est imputée sur un compte (Comptes bancaires), et ce qu'elle produit en comptabilité se lit sur l'écriture (Écritures comptables bancaires).

La chose à comprendre avant tout le reste​

La liste n'applique aucun filtre de statut de rapprochement. Tous les statuts sont rendus tant que vous n'en demandez pas un en particulier, et cela compte surtout pour partially_matched : une ligne à moitié imputée reste listée jusqu'à ce que son reste soit imputé ou écarté. Une file de rapprochement construite sur cet endpoint ne peut donc pas perdre silencieusement un mouvement à moitié réglé.

L'archivage n'est pas une exception non plus. Les lignes archivées sont rendues elles aussi : un archived absent ne restreint rien. C'est ?archived=false qui demande les lignes actives, et ?archived=true les seules archivées. La ressource des comptes bancaires applique exactement la même règle.

Autrement dit, hors l'entreprise du chemin, cet endpoint ne restreint rien de lui-même : ce que vous n'avez pas demandé, vous le recevez.

Lecture seule​

Cette ressource ne s'écrit pas. Il n'existe ni POST, ni PATCH, ni DELETE. Rien sur cette surface ne crée une opération, ne la corrige ni ne l'archive :

  • pour corriger une ligne issue d'un relevé - son montant, sa date, son libellé - passez par la revue du relevé qui l'a produite (PATCH /api/v1/bank_statements/{id}/lines) ;
  • une ligne issue d'une synchronisation est ce que la banque a transmis, et elle ne se corrige pas ;
  • pour changer ce qu'une opération produit en comptabilité, changez le rapprochement qui la produit. L'écriture suit : confirmer un rapprochement réécrit ses lignes sur place, d'autres chemins la détruisent et la reprojettent. Dans les deux cas c'est le rapprochement que vous modifiez, jamais l'écriture.

Aucun appel de cette page n'étant une écriture, l'en-tête Idempotency-Key n'y a pas d'objet.

Deux sous-ressources, elles, s'écrivent - et ce sont les seuls chemins par lesquels une opération change d'état de rapprochement : bank_operations/{id}/allocations brouillonne une affectation, bank_operations/{id}/reconciliation la solde. Les deux sont décrites dans Affectations et rapprochement.

Les endpoints​

  • GET /api/v1/workspaces/{workspace_id}/companies/{company_id}/bank_operations - lister les opérations d'une entreprise
  • GET /api/v1/bank_operations/{id} - lire une opération

Comme sur les autres ressources bancaires, seule la liste est adressée par workspace et par entreprise. L'endpoint portant un identifiant n'a pas de workspace_id dans le chemin : l'identifiant est résolu sur l'ensemble des workspaces rattachés à votre client OAuth. Les deux lectures demandent le scope read.

La liste est triée par operation_date décroissante, la plus récente d'abord, et les ex aequo sont départagés par identifiant décroissant - donc la pagination ne répète ni ne saute de ligne quand une banque envoie plusieurs mouvements le même jour.

Les champs​

Une opération porte vingt-deux champs, tous présents dans chaque réponse.

ChampCe qu'il porte
bank_account_idLe compte bancaire sur lequel le mouvement a eu lieu
statement_idLe relevé dont la ligne a été extraite, ou null si elle vient d'une synchronisation. null aussi sur une ligne statement dont le fichier a été supprimé depuis : supprimer un relevé annule le statement_id de chaque ligne qu'il n'a pas pu emporter, laissant une orpheline qui garde origin: "statement" et n'est volontairement plus comptabilisable. Un null ici ne signifie donc pas "venue d'une synchronisation" - lisez origin pour cela
amountLe montant signé, publié comme une chaîne à quatre décimales. Voir la section ci-dessous
currency_codeLa devise du mouvement, en ISO 4217
directionincoming ou outgoing. C'est le signe de amount dit en toutes lettres : les deux ne sont jamais en désaccord
operation_dateLa date à laquelle la banque a comptabilisé le mouvement. C'est la date de tri de la liste
value_dateLa date de valeur, quand la source en fournit une. Souvent null
descriptionLe libellé de la banque, un IBAN qu'il cite étant masqué - voir ci-dessous
clean_descriptionLe nom de contrepartie normalisé, quand il a pu être dérivé du libellé. Même masquage que description
categoryLa catégorie du fournisseur de données, transmise telle quelle. null si aucune n'a été fournie
operation_typeL'instrument du mouvement quand la source le nomme : card, deferred_debit_card, transfer, direct_debit, check, withdrawal, deposit, open_banking ou unknown
pendingLa banque annonce encore le mouvement comme provisoire - une autorisation qui peut encore être retirée. Tant que c'est le cas, aucune écriture n'est créée ni reprojetée pour cette ligne. Il n'en découle pas que projection.posted vaille false : une écriture qu'elle porte déjà est laissée intacte - voir projection
reconciliation_statusunmatched, proposed, partially_matched, matched ou rejected. C'est le champ sur lequel la liste se filtre
originsynchronized pour une ligne issue d'une synchronisation, statement pour une ligne issue d'un relevé. Il n'existe pas d'opération créée à la main. Une ligne statement existe dès que l'extraction l'a lue, que le relevé ait été validé ou non - ce champ ne dit donc rien de projection.posted
archivedLa ligne a été sortie du périmètre de travail. La liste ne les exclut pas pour autant - voir le filtre archived ci-dessous. Même règle de projection qu'une ligne pending : rien n'est créé ni reprojeté pour elle, et ce qu'elle porte déjà est conservé
projectionOù la ligne a atterri en comptabilité. Voir ci-dessous
reconciliationCe que les rapprochements confirmés ont imputé, et ce qui reste. Voir ci-dessous
versionLe jeton d'optimisme que POST /api/v1/bank_operations/{id}/reconciliation exige en retour sous le nom expected_version. Un entier, qui bouge à chaque changement du jeu d'affectations, y compris ceux qui laissent reconciliation_status là où il était. Voir Affectations et rapprochement

id, company_id, created_at et updated_at complètent la liste.

Un IBAN cité dans un libellé n'est jamais publié en entier. Les banques recopient souvent le compte de la contrepartie dans le texte d'un virement ou d'un prélèvement. Dans description et clean_description, un IBAN d'un seul tenant, ou groupé par blocs dès lors que sa clé de contrôle est valide, est remplacé par la même forme que iban_masked : FR7630006000011234567890189 devient FR*********************0189. Le reste du libellé est rendu tel que la banque l'a transmis. La même règle s'applique au label de l'écriture embarquée par include=bank_accounting_entry.

amount est une chaîne, et c'est délibéré​

"amount": "300.0000"

Quatre décimales, et le type JSON est une chaîne, pas un nombre. La raison est arithmétique : la colonne accepte dix-neuf chiffres significatifs, qu'un flottant double précision ne peut pas porter sans les altérer. Publier un nombre JSON ferait ressortir la plus grande valeur acceptée sous la forme 1.0e15.

Deux conséquences pour vous :

  • parsez-la en décimal, jamais en flottant, avec le type décimal de votre langage ;
  • la même ligne lue ailleurs porte exactement la même chaîne. GET /api/v1/bank_statements/{id}/lines publie cette colonne au même format, et c'est le format auquel une revue de relevé vous laisse l'écrire.

Le signe suit le sens : une opération entrante est positive, une sortante est négative. Vous n'avez pas à redériver le signe depuis direction.

Les deux montants de reconciliation, eux, sont bien des nombres JSON à deux décimales : ce sont des valeurs calculées, aucune n'est une colonne stockée, et aucune ne se réécrit.

reconciliation : ce qui est imputé, et ce qui reste​

"reconciliation": {
"status": "partially_matched",
"allocated_amount": 200.00,
"unallocated_remainder": 100.00,
"allocations_count": 1
}

Seuls les rapprochements confirmés comptent. Une proposition que personne n'a confirmée ne compte dans aucun des deux montants, et une proposition rejetée non plus : ni l'une ni l'autre n'a déplacé d'argent. allocations_count compte les mêmes rapprochements confirmés que allocated_amount additionne.

Les deux montants s'additionnent pour faire la valeur absolue de amount, arrondie à deux décimales. C'est la relation à retenir : allocated_amount est ce que les rapprochements ont réglé, unallocated_remainder est ce qui n'est encore imputé nulle part - et, sur une ligne comptabilisée, c'est exactement le montant que porte la ligne d'attente de son écriture.

L'arrondi fait partie de la relation, il ne la dégrade pas. Les deux chiffres sont calculés au quantum comptable de deux décimales, alors que amount en publie quatre : sur une opération de 300.0001 dont rien n'est imputé, la somme vaut 300.00 et non 300.0001. Le centième de centime manquant n'est pas de l'argent perdu, c'est une précision qu'aucune ligne de grand livre ne peut porter. N'attendez donc pas l'égalité aux quatre décimales de amount : vérifiez-la après arrondi à deux.

Ce que vous devez faire pour que ces montants bougent est décrit dans Affectations et rapprochement : seul un rapprochement confirmé les déplace.

status reprend reconciliation_status. Le champ est publié aux deux endroits parce que c'est celui sur lequel la liste se filtre, et qu'un lecteur qui travaille dans reconciliation ne devrait pas avoir à remonter d'un niveau pour le lire.

projection : si la ligne a atteint le grand livre​

"projection": {
"bank_accounting_entry_id": 44120,
"posted": true,
"exported": false,
"incident_code": null
}

Une écriture n'existe que si la comptabilisation a réussi, donc posted et la présence de bank_accounting_entry_id sont un seul et même fait. Il n'y a pas d'état où une opération porterait une écriture sans être comptabilisée.

exported dit si cette écriture est sortie des livres - exportée vers un logiciel comptable, ou livrée à un cabinet. Il vaut false et non null sur une opération non comptabilisée : une ligne qui n'est jamais entrée dans les livres n'en est certainement pas sortie. posted et exported sont calculés à chaque lecture, jamais stockés. incident_code, lui, est enregistré au moment de chaque tentative - voir ci-dessous.

posted décrit l'ÉCRITURE, jamais l'éligibilité actuelle de la ligne. Une ligne pending ou archived ne fait l'objet d'aucune création ni reprojection - mais une écriture qu'elle portait déjà est conservée telle quelle. Donc posted: true à côté de pending: true ou archived: true se lit, et se lit ainsi : l'écriture a été passée quand la ligne était dénouée et active, et elle n'est pas rafraîchie en ce moment. Traitez-la comme l'enregistrement de ce qui a été comptabilisé, non comme une projection vivante de la ligne. Le moment où une telle ligne recommence à être projetée sort du périmètre de ce contrat. Elle peut aussi porter skipped_pending ou skipped_archived : sa dernière tentative a été écartée, et l'écriture plus ancienne est conservée.

Une ligne issue d'un relevé n'est par ailleurs comptabilisable qu'une fois ce relevé validé.

incident_code : ce qu'a rencontré la dernière tentative​

incident_code dit pourquoi la dernière tentative de comptabilisation de cette ligne n'a pas abouti, ou vaut null. Il est enregistré au moment où la tentative a lieu, puis rendu tel quel : il n'est jamais déduit à la lecture. La clé est toujours présente.

  • Une tentative refusée ou écartée enregistre son code.
  • Une tentative réussie le remet à null, y compris quand il n'y avait rien à réécrire.
  • Quand deux tentatives se chevauchent, la plus récente l'emporte : une tentative plus ancienne qui se termine en retard n'écrase jamais un résultat plus récent.
  • Une tentative interrompue par une erreur imprévue n'enregistre rien et laisse la valeur précédente.
  • Une ligne dont aucune tentative n'a eu lieu depuis la publication de ce champ porte null : aucun historique n'est reconstitué.
incident_codeCe qu'a rencontré la dernière tentative
provider_unavailableLe fournisseur ne donnait plus accès au compte bancaire
not_activatedLe compte bancaire n'était pas activé pour la comptabilisation
missing_ledgerAucun journal comptable n'était rattaché au compte bancaire
missing_bank_account_codeAucun code de compte comptable n'était renseigné sur le compte bancaire
missing_suspense_account_codeAucun compte d'attente n'était renseigné sur le compte bancaire
skipped_archivedLa ligne était archivée : rien n'a été créé ni reprojeté
skipped_pendingLa ligne était encore provisoire : rien n'a été créé ni reprojeté
skipped_cross_source_heldLa ligne est retenue parce qu'une ligne de relevé et une ligne synchronisée peuvent décrire le même mouvement. Elle attend un choix humain, et rien n'est comptabilisé deux fois
unsupported_currencyLa devise n'est pas l'euro
direction_amount_mismatchLe signe de amount et direction étaient en désaccord. Une opération enregistrée ne pouvant pas porter ce désaccord (voir direction), ce code n'est jamais enregistré
zero_amountLe montant est nul
missing_party_accountUn rapprochement confirmé ne désigne aucun tiers, ou le tiers n'a pas de compte auxiliaire
invalid_entryL'écriture n'a pas pu être enregistrée ; rien n'a changé

Quand plusieurs conditions manquent au compte bancaire, seule la première est enregistrée, dans l'ordre du tableau ; readiness.missing les liste toutes.

Ce n'est pas l'état actuel du compte. incident_code décrit une tentative passée, alors que les conditions de comptabilisation du compte bancaire sont calculées à chaque lecture dans readiness (Comptes bancaires). Les deux peuvent diverger : un missing_bank_account_code reste sur la ligne après que vous avez renseigné le code de compte, jusqu'à la tentative suivante. Pour savoir si un compte peut comptabiliser maintenant, lisez readiness.

posted: true à côté d'un incident_code se lit aussi. L'écriture vient d'une tentative antérieure réussie, et la dernière tentative n'a pas abouti : l'écriture existante n'a pas été touchée. Une écriture qui a déjà quitté les livres n'est par ailleurs jamais réécrite : si sa réimputation ne trouve aucun compte de tiers, la tentative enregistre missing_party_account et l'écriture reste telle quelle.

Une ligne qui n'a pas été tentée n'enregistre rien. Elle garde null, ou le code de sa dernière tentative, et rien n'est déduit à la lecture :

  • la synchronisation ne tente pas une ligne dont le compte n'a pas de journal, n'est pas activé ou n'est plus fourni par le fournisseur - c'est alors readiness du compte qui dit pourquoi elle n'est pas comptabilisée. Elle ne tente pas non plus une ligne dont la connexion bancaire est disconnected, ni une ligne archivée ;
  • une ligne synchronisée sur un compte auquel il ne manque qu'un code de compte est, elle, tentée, et enregistre missing_bank_account_code ou missing_suspense_account_code ;
  • la validation d'un relevé tente chaque ligne qu'elle comptabilise, dans l'ordre, et s'arrête au premier refus : la ligne refusée enregistre son code, la validation entière est annulée (posting_failed), et les autres lignes gardent leur valeur précédente. Si le compte n'a pas de journal, n'est pas activé ou n'est plus fourni, la validation est refusée (account_not_ready) avant toute tentative, et aucune ligne n'enregistre de code.

missing_ledger, not_activated et provider_unavailable n'apparaissent donc que si le compte a changé d'état pendant une tentative en cours.

Lire une opération​

Un token read suffit.

curl https://app.scribee.tech/api/v1/bank_operations/30144 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": {
"id": 30144,
"company_id": 34,
"bank_account_id": 811,
"statement_id": 9001,
"amount": "300.0000",
"currency_code": "EUR",
"direction": "incoming",
"operation_date": "2026-07-14",
"value_date": "2026-07-15",
"description": "VIR SEPA ACME CORP",
"clean_description": "ACME CORP",
"category": "income",
"operation_type": "transfer",
"pending": false,
"reconciliation_status": "partially_matched",
"origin": "statement",
"archived": false,
"projection": {
"bank_accounting_entry_id": 44120,
"posted": true,
"exported": false,
"incident_code": null
},
"reconciliation": {
"status": "partially_matched",
"allocated_amount": 200.00,
"unallocated_remainder": 100.00,
"allocations_count": 1
},
"version": 6,
"created_at": "2026-07-14T06:12:04+02:00",
"updated_at": "2026-07-14T06:12:04+02:00"
}
}

Filtrer la liste​

Neuf filtres, tous facultatifs et tous cumulables.

ParamètreCe qu'il restreint
originsynchronized ou statement
bank_account_idUn compte bancaire de l'entreprise
directionincoming ou outgoing
reconciliation_statusUn statut de rapprochement
operation_date_from, operation_date_toUn intervalle sur operation_date, bornes incluses
pendingtrue pour les seules lignes provisoires, false pour les seules lignes définitives
statement_idLes lignes extraites d'un relevé donné
archivedtrue pour les seules lignes archivées, false pour les seules lignes actives. Absent, les deux sont rendues - ce n'est pas un synonyme de false

Une valeur non reconnue ne renvoie rien, et ne provoque pas d'erreur. ?direction=sideways rend une page vide, pas un 400. C'est la convention de cette surface : le résultat est l'ensemble des opérations dont le sens est sideways, qui est vide.

Les deux bornes de date sont strictement ISO 8601 (AAAA-MM-JJ). Une valeur d'une autre forme ne correspond à rien plutôt que d'être devinée : une date écrite jour en premier ne sera jamais lue mois en premier, et vous ne recevrez donc jamais une fenêtre silencieusement différente de celle que vous avez demandée.

page et per_page paginent comme partout ailleurs (20 par défaut, 100 au maximum).

Les include​

Quatre associations peuvent être embarquées, séparées par des virgules : ?include=bank_account,bank_statement,bank_accounting_entry,allocations.

IncludeCe que vous recevez
bank_accountLa charge utile complète du compte bancaire, readiness compris - la même forme que rendent les endpoints des comptes
bank_statementLe relevé dont la ligne a été extraite. null sur une ligne synchronisée, qui n'a pas de fichier derrière elle
bank_accounting_entryL'écriture comptable, sans ses lines : lisez la ressource des écritures pour celles-ci. null sur une ligne non comptabilisée, c'est-à-dire dès que projection.posted vaut false
allocationsToutes les affectations de l'opération, triées de la plus ancienne à la plus récente - et pas seulement les confirmées. C'est le même jeu que rend GET /api/v1/bank_operations/{id}/allocations sans filtre. Les deux montants de reconciliation, eux, restent limités aux confirmées : ce sont des montants, et une proposition n'en a déplacé aucun

Une valeur qui n'est aucune des quatre est ignorée. Sans include, aucune de ces clés n'est présente dans la réponse - elles ne sont pas publiées à null, elles sont absentes.

Les erreurs​

CodeQuand
400page n'est pas un entier supérieur ou égal à 1, ou désigne une page au-delà de la dernière. per_page illisible ne provoque pas d'erreur : il retombe sur la valeur par défaut
401Aucun jeton, ou un jeton invalide ou expiré
403Trois causes. Votre jeton ne porte pas le scope read (un jeton write ou destroy seul est valide et se fait refuser ici) ; votre application n'a pas d'accès sur ce workspace ; ou la fonctionnalité de rapprochement bancaire y est désactivée
404Sur la liste : aucune entreprise de cet identifiant dans le workspace du chemin. Sur la lecture : aucune opération de cet identifiant accessible à votre jeton

Un 404 ne vous dit jamais qu'une ressource existe ailleurs. Une entreprise d'un autre workspace répond comme une entreprise inexistante - y compris quand votre jeton a aussi accès à cet autre workspace - et une opération hors de vos accès répond comme une opération inexistante. Aucune de ces réponses ne peut servir à sonder l'existence d'un identifiant.

Sur la lecture par identifiant, l'accessibilité est tranchée avant la fonctionnalité : une opération que votre jeton ne peut pas lire répond 404, jamais 403. Un 403 sur cet endpoint ne confirme donc jamais qu'un identifiant existe.

Le scope, lui, est vérifié avant tout le reste - y compris avant l'existence de l'opération. C'est ce qu'il faut savoir pour diagnostiquer un 403 sur cette ressource : un jeton sans read est refusé avant même que l'identifiant du chemin soit résolu, donc vous obtenez 403 sur un identifiant inexistant tout comme sur un identifiant valide. Si un 403 vous surprend, vérifiez d'abord les scopes de votre jeton : vous chercheriez sinon un problème de droits sur un enregistrement qui n'a jamais été en cause.

Référence API​