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 entriesGET /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.
| Field | What it carries |
|---|---|
bank_operation_id | The 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_date | The 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_kind | bank_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 |
reference | A 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, ...) |
label | Derived 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_id | The accounting ledger the entry was posted to. It is the bank account's ledger as at the projection |
total_debit, total_credit | Equal 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:
- the bank line, for the whole magnitude - the accounting account configured on the bank account (
512999here); - one counterparty line per confirmed reconciliation - only one here,
411ACMEfor 200.00; - the suspense line, for the remainder only -
471000for 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:
| Attribute | What it carries |
|---|---|
account_number | The chart-of-accounts number this line was posted to |
label | The line's label. The reconciliation's on a counterparty line, the entry's on the bank line and on the suspense line |
debit_amount | Positive or zero |
credit_amount | Positive or zero |
tax_code | Always null on these entries: the bank projection attaches no tax code to any of its lines |
explanation | Why 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.
| Key | What it carries |
|---|---|
source | allocation, 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_id | The id of the rule that decided the line, when source is company_rule or tenant_rule. null otherwise |
rule_name | That rule's name at the time it decided. null when rule_id is |
reason | Text 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_kindisbank_operation_correctionwritten 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:
| Filter | Values | What it keeps |
|---|---|---|
bank_operation_id | an integer | The 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_from | an ISO 8601 date | The entries whose entry_date is on or after that date, bounds included |
entry_date_to | an ISO 8601 date | The entries whose entry_date is on or before that date, bounds included |
ledger_id | an integer | The entries posted to one ledger |
bank_account_id | an integer | The 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
includeon 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 carrylines, 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
| Status | error | When |
|---|---|---|
400 | bad_request | A page that is not an integer greater than or equal to 1, or a page beyond the last page of a non-empty collection |
403 | forbidden | Your OAuth client holds no grant on this workspace, or the token does not carry the read scope |
404 | not_found | The 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.