Skip to main content

Bank accounting entries

A bank accounting entry (bank_accounting_entry) is the accounting projection of a bank operation: the ledger lines Scribee posted for that operation, with their accounts and their amounts. This is the resource that tells you where a movement was posted, not only that it was.

A bank account carries the configuration without which nothing is projected (Bank accounts), an accounting ledger receives the entry (Accounting ledgers), and this resource is what the projection produces.

The thing to understand before anything else​

lines is the accounting itself, and it is published on every row of the list, never behind an include. The entry's two totals do not tell you where the money went: an operation of 300.00 with 200.00 reconciled carries a counterparty line of 200.00 and a suspense line of 100.00, and those two lines have exactly the same totals as an entry that had put everything in suspense. If you read this resource, read lines.

Read-only​

This resource is never written to. There is no POST, no PATCH and no DELETE. An entry is recomputed from its operation's confirmed reconciliations rather than edited in place: to change an entry, change the reconciliation that produces it. Since no call on this page is a write, the Idempotency-Key header has no purpose here.

An entry exists only when posting succeeded. A posting incident is therefore never representable here - there is no failed entry to read. What an unposted operation tells you is that it is unposted: on its own payload, which you can have embedded here with ?include=bank_operation, projection.posted is false and projection.bank_accounting_entry_id is null.

The reason for a failure is read on the operation, never here. projection.incident_code says what the line's last posting attempt ran into (Bank operations). It is not the account's current state: the bank account's posting conditions are read in readiness.missing (Bank accounts) - that is where by far the most common cause sits, an incomplete configuration.

Entries sourced from an invoice are never returned here. An entry is either invoice-sourced or bank-operation-sourced, never both, and this resource publishes only the second kind: an invoice entry addressed by its id answers 404, as though it did not exist.

The endpoints​

  • GET /api/v1/workspaces/{workspace_id}/companies/{company_id}/bank_accounting_entries - list a company's entries
  • GET /api/v1/bank_accounting_entries/{id} - read an entry

As on the other banking resources, only the list is addressed per workspace and per company. The endpoint carrying an id has no workspace_id in the path: the id is resolved across every workspace attached to your OAuth client. Both reads require the read scope.

An entry carries fourteen fields, all present in every response: id, company_id, bank_operation_id, entry_date, entry_kind, reference, label, ledger_id, total_debit, total_credit, exported, lines, created_at and updated_at.

FieldWhat it carries
bank_operation_idThe operation this entry is the projection of. An operation has at most one bank_operation entry, plus any corrections, which share its bank_operation_id
entry_dateThe date of the operation, not the posting date. created_at gives you the second one. On a bank_operation_correction, the later of the operation date and the corrected entry's lock date: a correction never predates the lock
entry_kindbank_operation for the operation's primary projection, bank_operation_correction for a correction emitted against a locked entry. Read it rather than assume it: a new kind of projection would arrive as a new value here, not as a hidden row
referenceA reference assigned by Scribee, built from its own identifiers only: BQ, then the operation's year and month, then a number of at least four digits - for example BQ-2026-07-0144. It is set when the entry is created and is not rewritten when the entry is recomputed. To link an entry to its operation, use bank_operation_id. On a bank_operation_correction, the corrected entry's reference followed by -C and the correction's rank (-C1, -C2, ...)
labelDerived from the operation's label (description, failing that clean_description), truncated to 255 characters. If the operation carries neither, Opération bancaire followed by the operation's id. On a bank_operation_correction, Régularisation - followed by the corrected entry's label, the whole truncated to 255 characters
ledger_idThe accounting ledger the entry was posted to. It is the bank account's ledger as at the projection
total_debit, total_creditEqual on every projected entry: the balance holds by construction, and no adjustment line is added to obtain it

:::warning Behaviour change Until now, reference took the operation's external identifier when it carried one, otherwise BANK-OP- followed by the operation's id, and the fallback label of an operation without a description took that same external identifier. Both are now built from Scribee identifiers only, and existing entries were rewritten once to these new values. If your integration stored a reference or a label, the value you read back is no longer the same: match your data on id or bank_operation_id, which have not changed. :::

Reading an entry​

A read token is enough.

curl https://app.scribee.tech/api/v1/bank_accounting_entries/44120 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": {
"id": 44120,
"company_id": 34,
"bank_operation_id": 30144,
"entry_date": "2026-07-14",
"entry_kind": "bank_operation",
"reference": "BQ-2026-07-0144",
"label": "ACME CORP",
"ledger_id": 4,
"total_debit": 300.00,
"total_credit": 300.00,
"exported": false,
"lines": [
{
"account_number": "512999",
"label": "ACME CORP",
"debit_amount": 300.00,
"credit_amount": 0.00,
"tax_code": null,
"explanation": {
"source": "allocation",
"rule_id": null,
"rule_name": null,
"reason": "bank account 512999"
}
},
{
"account_number": "411ACME",
"label": "Facture 90210",
"debit_amount": 0.00,
"credit_amount": 200.00,
"tax_code": null,
"explanation": {
"source": "allocation",
"rule_id": null,
"rule_name": null,
"reason": "confirmed allocation 7001"
}
},
{
"account_number": "471000",
"label": "ACME CORP",
"debit_amount": 0.00,
"credit_amount": 100.00,
"tax_code": null,
"explanation": {
"source": "suspense",
"rule_id": null,
"rule_name": null,
"reason": "unallocated remainder on account 471000"
}
}
],
"created_at": "2026-07-14T06:12:04+02:00",
"updated_at": "2026-07-14T06:12:04+02:00"
}
}

lines: how to read the ledger​

That example is an incoming operation of 300.00 with 200.00 reconciled against an invoice. Three lines, in this exact order, which is the order the projection writes them in and the order an accountant reads them in:

  1. the bank line, for the whole magnitude - the accounting account configured on the bank account (512999 here);
  2. one counterparty line per confirmed reconciliation - only one here, 411ACME for 200.00;
  3. the suspense line, for the remainder only - 471000 for the 100.00 not yet posted anywhere.

The third line exists only when something is left to post. A fully reconciled operation has no suspense line at all; an operation with no confirmed reconciliation has a suspense line carrying the whole amount. A single suspense line of 300.00 on that example would not be a simplification, it would be wrong: Scribee never produces that shape.

Six attributes per line, all present:

AttributeWhat it carries
account_numberThe chart-of-accounts number this line was posted to
labelThe line's label. The reconciliation's on a counterparty line, the entry's on the bank line and on the suspense line
debit_amountPositive or zero
credit_amountPositive or zero
tax_codeAlways null on these entries: the bank projection attaches no tax code to any of its lines
explanationWhy the line was posted to that account - see below. An object, or null

An IBAN quoted in a label is never published in full. The entry's label and the label of each of its lines carry over the operation's label; an IBAN it quotes is returned there in the form of iban_masked - FR*********************0189 - under the rule described for bank operations. The entry lines in the bank_operation.reconciled webhook are returned the same way.

Both amounts are always positive or zero, and exactly one of the two sides is non-zero. A line never carries a single signed amount: read the side, not the sign. The operation's direction decides the side: an incoming operation debits the bank and credits the counterparty, an outgoing one does the reverse.

The lines have no id, and that is deliberate. They are rewritten wholesale on every new projection, so an id you had stored would name a line that no longer exists. What you can store is the entry's id.

explanation: why a line is on its account​

Every line says why it was posted to its account. The answer follows the order in which the projection picks an account - confirmed reconciliation, then company rule, then workspace rule (Bank imputation rules), then suspense account - and takes the form of an object with four keys, always present. The lines of a correction sit outside that order: they carry their own source, correction.

KeyWhat it carries
sourceallocation, company_rule, tenant_rule, suspense or correction. The bank line and the counterparty lines of a confirmed reconciliation carry allocation; a remainder line posted by a rule carries company_rule or tenant_rule; a remainder no rule reached carries suspense; every line of an entry whose entry_kind is bank_operation_correction carries correction
rule_idThe id of the rule that decided the line, when source is company_rule or tenant_rule. null otherwise
rule_nameThat rule's name at the time it decided. null when rule_id is
reasonText meant for a human, written in English whatever the language of the user behind the write. Do not parse it: its wording is not part of the contract

A line posted by a rule:

{
"account_number": "651600",
"label": "PRLV SEPA SAAS ACME",
"debit_amount": 89.90,
"credit_amount": 0.00,
"tax_code": null,
"explanation": {
"source": "company_rule",
"rule_id": 88,
"rule_name": "Abonnements SaaS",
"reason": "description contains \"SAAS\""
}
}

The explanation is recorded when the line is written, and is never recomputed. A rule renamed, modified or deleted since still reads here as it stood when it decided the line: rule_id can therefore name a rule that no longer exists, and rule_name can differ from the rule's current name. That is deliberate - the explanation describes what produced the line, not what today's rules would produce. Only a new write of the line replaces its explanation: a new projection, or a reconciliation confirmed or undone that moves the line, gives it the explanation of that write.

A correction line:

{
"account_number": "471000",
"label": "Régularisation - VIR SEPA RECU ACME SAS",
"debit_amount": 1200.00,
"credit_amount": 0.00,
"tax_code": null,
"explanation": {
"source": "correction",
"rule_id": null,
"rule_name": null,
"reason": "Adjustment of entry BQ-2026-03-1001: imputation adjusted on account 471000."
}
}

No rule decided a correction line: it carries the difference, account by account, between what the operation's entries have already posted and what the operation now justifies. rule_id and rule_name are therefore null, and reason names the corrected entry by its reference - the one the correction's own reference extends with -C and its rank. The line's label and its explanation are two different things: the label is what the line carries in the ledger, the explanation says why it is on its account.

explanation is null when no reason was recorded for the line, which happens in three cases:

  • a line written before Scribee recorded this explanation, when its reason could not be derived without ambiguity from what was stored or its entry was already frozen - a remainder line, in particular, is never reconstructed, even on the suspense account, since a rule may have decided it and changed since;
  • a line of an entry whose entry_kind is bank_operation_correction written before Scribee recorded this explanation on corrections: it is not reconstructed;
  • the balance that undoing a reconciliation leaves on a counterparty line that cited that reconciliation.

A null does not mean the line is wrong.

The amounts and their precision​

The entry's amounts - total_debit, total_credit, and each line's two amounts - are JSON numbers at two decimals. That is the precision the projection is computed at, and the one this resource's contract states.

An amount on this page therefore reads differently from a bank operation's amount, and the difference is deliberate. If you ask for ?include=bank_operation, the embedded operation publishes its amount as a string at four decimals - "300.0000", quotes included - because that is the exact value as stored and an operation is read back to the last decimal. A client parsing that field as a JSON number would break. The two forms coexist on purpose: the entry's figures are derived and rounded, the operation's is stored as is.

exported: what has left the books​

exported is derived on every read, never stored: it therefore cannot be stale. It is true as soon as the entry has been exported to an accounting package, or delivered to one.

An exported entry is frozen. A new projection will not rewrite it and will not delete it, and a reconciliation change aimed at it is refused with a 422 and the code entry_already_exported - a refusal that belongs to the reconciliation endpoints, not to the ones on this page, which never refuse a read for that reason.

Listing a company's entries​

curl "https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/companies/YOUR_COMPANY_ID/bank_accounting_entries?entry_date_from=2026-07-01&ledger_id=4" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

The list is returned from the most recent entry_date to the oldest, and within a tie from the largest id to the smallest; the data + meta envelope follows API conventions.

Pagination is offset-based, with page and per_page - 20 by default, 100 maximum - as on every other banking resource. There is no cursor. Offset pagination over a growing set can repeat or skip a row between two requests: bound your window with entry_date_from and entry_date_to rather than assuming a stable snapshot.

{
"data": [],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 5,
"total_count": 87
}
}

Five filters, all optional and combinable:

FilterValuesWhat it keeps
bank_operation_idan integerThe entries projected from one operation: its bank_operation entry, if there is one, and each of its bank_operation_correction corrections. Zero, one or several rows; entry_kind tells them apart
entry_date_froman ISO 8601 dateThe entries whose entry_date is on or after that date, bounds included
entry_date_toan ISO 8601 dateThe entries whose entry_date is on or before that date, bounds included
ledger_idan integerThe entries posted to one ledger
bank_account_idan integerThe entries whose source operation belongs to one bank account

bank_account_id deserves a word: the entry carries no bank account at all. This filter reaches it through the source operation, so it gives you exactly the entries of that account's operations.

A value a filter cannot read matches nothing, and that is not an error - not a 422, and not a silently wider window. A ledger_id that is not an integer, a date that is not in YYYY-MM-DD form, a parameter sent as an array: in all three cases the answer is a 200 with an empty collection. Both date bounds accept only ISO 8601: 14/07/2026 matches nothing rather than being guessed as 14 July or as 7 April.

An absent or empty parameter is not a filter: it restricts nothing.

Embedding the source operation​

?include=bank_operation adds the whole payload of its source operation to each entry, under the bank_operation key. Without that parameter the key is absent - not present and null. It is this resource's only include; any other value is ignored.

The embedded operation embeds nothing in turn: it carries neither its account, nor its statement, nor its entry. The payload therefore cannot nest indefinitely.

Which path to choose​

The same entry is reachable by two paths, and that overlap is intended. Bank operations publish an include named bank_accounting_entry that returns an eleven-key subset of it. This resource publishes those eleven keys, with exactly the same names and the same meaning, and adds lines, created_at and updated_at.

  • You are already walking the operations and want to see, along the way, whether each one is posted and on what totals: ask for the include on the operations and save yourself a call.
  • You want the entries for their own sake - with their own filters, their own pagination, and above all with lines: use this resource. The subset does not carry lines, so it cannot show you the breakdown.

The eleven shared keys cannot diverge between the two paths: it is one concept with a single spelling.

The errors​

StatuserrorWhen
400bad_requestA page that is not an integer greater than or equal to 1, or a page beyond the last page of a non-empty collection
403forbiddenYour OAuth client holds no grant on this workspace, or the token does not carry the read scope
404not_foundThe company or the entry is not reachable by your grants

A page beyond the end:

{
"error": "bad_request",
"message": "Le numéro de page dépasse le nombre de pages disponibles"
}

A page that is not a valid integer carries the message "Le numéro de page doit être un entier supérieur ou égal à 1". On an empty collection no page number overflows: the answer is a 200 with an empty collection.

A resource out of reach answers 404, never 403. Reachability is settled before the question of rights: an entry in a workspace your grants do not cover, an entry that does not exist, and an invoice-sourced entry all receive the same answer, so the response cannot be used to work out which of the three you met.

{
"error": "not_found",
"message": "La ressource demandée est introuvable"
}

The 403 is reserved for what is a matter of rights rather than reachability: a missing grant on the workspace named in the path, or an insufficient scope.

API reference​