Allocations and reconciliation
An allocation (bank_allocation) is a link between one bank operation and one invoice: the share of the movement that settles that invoice. An operation can carry several - a 300.00 transfer paying two invoices of 200.00 and 100.00 carries two - and it is this allocation set that decides what the operation produces in accounting.
Two resources share the subject, and the boundary between them is the thing to understand before writing a single line of code:
bank_allocationsdrafts. Creating an allocation moves no money.bank_operations/{id}/reconciliationsettles. This is the transition that writes the payments and recomputes the accounting entry.
The movement they hang off is described in Bank operations, and what the reconciliation produces in the books in Bank accounting entries.
The thing to understand before anything else
Creating an allocation is a draft, and nothing else. POST /api/v1/bank_operations/{id}/allocations writes a proposed row: no payment is created, no ledger line is touched, the invoice stays unpaid, and the amounts under reconciliation on the operation do not move - they only count confirmed allocations. That is deliberately cheap: you can build a split over several calls, read it back and correct it without any of those calls committing to anything.
Confirming is a convergence, not an append. POST /api/v1/bank_operations/{id}/reconciliation takes the complete allocation set you want confirmed, plus the operation's version. The set you send is the set that ends up confirmed:
- an allocation confirmed today and absent from the body is withdrawn;
- an allocation present at a different amount is withdrawn and re-confirmed at the new one;
- an allocation already confirmed at the requested amount is left untouched - so sending the same body twice writes nothing at all;
- an invoice named in the body that carries no allocation yet gets one, created and confirmed in the same call. You are not required to draft first.
A partial body is not a partial update. If you send only one of a split's two allocations, the other is withdrawn. And the call is all-or-nothing: the first sub-refusal fails the whole transaction, so a half-applied split is not a state this endpoint can produce.
DELETE /api/v1/bank_allocations/{id} deletes no row. An allocation is an audit record: this call writes status: "rejected" and returns the row as it now stands. That is what lets you tell an invoice that was never proposed from one that was proposed and then refused.
The endpoints
| Endpoint | What it does | Scope |
|---|---|---|
GET /api/v1/bank_operations/{bank_operation_id}/allocations | List an operation's allocations | read |
POST /api/v1/bank_operations/{bank_operation_id}/allocations | Draft an allocation | write |
PATCH /api/v1/bank_allocations/{id} | Change the drafted amount, or withdraw the allocation | write |
DELETE /api/v1/bank_allocations/{id} | Withdraw the allocation (writes rejected) | destroy or write |
POST /api/v1/bank_operations/{bank_operation_id}/reconciliation | Confirm the complete set | write |
DELETE /api/v1/bank_operations/{bank_operation_id}/reconciliation | Undo confirmations | destroy or write |
As on the other banking resources, none of these paths carries a workspace_id: the id is resolved across every workspace your OAuth client holds a grant on. An operation or an allocation outside your grants answers 404, exactly like one that does not exist.
Both creations - the POST on allocations and the POST on the reconciliation - require the Idempotency-Key header. Both DELETEs and the PATCH accept it without requiring it.
An allocation's fields
Eleven fields, all present in every response.
| Field | What it carries |
|---|---|
bank_operation_id | The operation this allocation draws on |
invoice_document_id | The invoice it settles. There is at most one active allocation per (operation, invoice) pair; a second is refused |
allocated_amount | The allocated amount, always non-negative, published as a JSON number at four decimals. See below |
status | proposed, confirmed, unconfirmed or rejected. See below |
score | The matcher's confidence, 0 to 1 at four decimals. An allocation you create yourself carries 1.0 |
rejection_reason | wrong_amount, wrong_counterparty, wrong_period or other, when the allocation was withdrawn with a reason. null otherwise |
explanation | Why this imputation. See below |
id, company_id, created_at and updated_at complete the list.
allocated_amount: a number, at four decimals
The direction of the movement is not read here. allocated_amount is a magnitude: a signed value is refused. The direction lives on the operation's direction, and publishing it twice would expose the two to contradicting each other.
Four decimals, and they are not the two of bank_operation.reconciliation.allocated_amount. These are two different numbers, not two roundings of one: the operation's is what the allocations posted, computed at the two-decimal accounting quantum, while this one is the raw figure the allocation carries, at the column's own scale. It is this one that a split's running total is measured over.
On write it is a string or a whole number, never a JSON float. A double cannot carry four decimals without altering them, so 0.30000000000004 is a shape that is refused rather than silently rounded away. Same rule as on a statement review.
"allocated_amount": 200.0
null is possible on read on a legacy row that named no amount. It is not a shape this API can produce.
status: four values, only one terminal
| Value | What it says |
|---|---|
proposed | A draft - a suggestion from the matcher, or an allocation you created. No money has moved |
confirmed | Money moved: a payment exists and the ledger line was re-imputed |
unconfirmed | A confirmation you undid. The allocation is still there and can be re-confirmed |
rejected | Terminal. The allocation was refused and will not come back |
rejected is the only value the bank_allocations resource writes. A confirmation goes through POST /api/v1/bank_operations/{id}/reconciliation, and an already-decided allocation never drops back to proposed.
unconfirmed is not rejected, and the distinction is the whole meaning of the two: undoing a confirmation is a reversible decision, refusing it is not.
explanation: why this imputation
"explanation": {
"source": "allocation",
"rule_id": null,
"rule_name": null,
"reason": "operator allocation of 200.0000 to invoice 90210"
}
On this resource source is always allocation: an operator allocation outranks every rule in the precedence chain, so no rule is named and both rule_id and rule_name are null.
reason is a sentence meant for a human and must not be parsed by a program. Its wording is not a contract.
Drafting an allocation
curl -X POST https://app.scribee.tech/api/v1/bank_operations/30144/allocations \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Idempotency-Key: 6f1d4a0e-6a1e-4f3f-9a7d-2f0c9d5b8c41" \
-H "Content-Type: application/json" \
-d '{
"invoice_document_id": 90210,
"allocated_amount": "200.00"
}'
{
"data": {
"id": 7001,
"company_id": 34,
"bank_operation_id": 30144,
"invoice_document_id": 90210,
"allocated_amount": 200.0,
"status": "proposed",
"score": 1.0,
"rejection_reason": null,
"explanation": {
"source": "allocation",
"rule_id": null,
"rule_name": null,
"reason": "operator allocation of 200.0000 to invoice 90210"
},
"created_at": "2026-08-21T14:00:00+02:00",
"updated_at": "2026-08-21T14:00:00+02:00"
}
}
Both body fields are required. The response is 201.
The invoice must belong to the operation's own company, and the same domain rules the application enforces apply here:
- an incoming movement only settles a sales invoice, an outgoing one only a purchase invoice;
- the invoice's currency and the movement's must be identical;
- the invoice must still be open and payable;
- the operation must still be allocatable:
unmatched,proposedorpartially_matched. Amatchedoperation has nothing left to allocate.
Two distinct refusals concern the invoice, both under details.invoice_document. A body without invoice_document_id receives the message est obligatoire. An invoice_document_id that designates no invoice of the operation's company receives the message est introuvable dans l'entreprise de cette opération: an invoice of another company, an id that does not exist, or a value that is neither a whole number nor a string - null, an object, a list, a boolean. All of these cases receive the same refusal, deliberately: the response therefore cannot be used to probe for an invoice id.
The active allocations cannot sum beyond the movement. Drafts count towards that ceiling exactly as confirmations do - only rejections are excluded - and going over is refused with allocation_exceeds_operation:
{
"error": "unprocessable_entity",
"code": "allocation_exceeds_operation",
"message": "La validation a échoué",
"details": {
"base": ["Les affectations de cette opération bancaire solderaient plus que le montant du mouvement"]
}
}
The body is checked in this order, and only the first refusal is answered: allocated_amount (details.allocated_amount), then the invoice (details.invoice_document), then the movement's ceiling (allocation_exceeds_operation, details.base), then the pair already allocated and the domain rules above. Those last two refusals are validation_failed responses that carry their reason in message, with no details key.
Correcting or withdrawing a draft
PATCH /api/v1/bank_allocations/{id} accepts allocated_amount, status and rejection_reason. At least one of the first two is required; the fields you leave out keep their stored values.
curl -X PATCH https://app.scribee.tech/api/v1/bank_allocations/7001 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"allocated_amount": "150.00"}'
The movement's ceiling is re-checked, excluding the share this allocation already claims: raising an allocation to the whole movement is therefore accepted when nothing else claims any of it.
Only a proposed or an unconfirmed allocation can be re-sized, and the refusal is keyed on the allocated_amount field:
- a
confirmedallocation is refused. Its amount is not the only record of what it settled: confirming wrote an invoice payment and re-imputed a ledger line, and no write to the column follows them - you would be left with an allocation that agrees with neither its payment nor its line. To change a confirmed allocation's amount, send the complete intended set toPOST /api/v1/bank_operations/{id}/reconciliation, which withdraws and re-confirms in one transaction; - a
rejectedallocation is refused because that state is terminal: a refusal is not rewritten. Create a new allocation for the pair instead; - an
unconfirmedallocation stays re-sizable.DELETE /api/v1/bank_operations/{id}/reconciliationhas already destroyed its payment and reversed its line, so the row asserts nothing about money and is reworked like a draft.
An operation that settles an instruction of a payment batch also refuses any resize of its allocations, proposed and unconfirmed included (An operation that settles a payment batch).
status accepts only rejected. Any other value - confirmed included - is refused on the status field.
DELETE /api/v1/bank_allocations/{id} does the same thing with no request body, and returns the allocation now rejected:
{
"data": {
"id": 7001,
"company_id": 34,
"bank_operation_id": 30144,
"invoice_document_id": 90210,
"allocated_amount": 200.0,
"status": "rejected",
"score": 1.0,
"rejection_reason": "wrong_amount",
"explanation": {
"source": "allocation",
"rule_id": null,
"rule_name": null,
"reason": "operator allocation of 200.0000 to invoice 90210"
},
"created_at": "2026-08-21T14:00:00+02:00",
"updated_at": "2026-08-21T15:30:00+02:00"
}
}
Only a proposed and an unconfirmed allocation can be withdrawn here. A confirmed one is refused: the money has to come back first, through DELETE /api/v1/bank_operations/{id}/reconciliation. A rejected one is refused too - that state is terminal.
rejection_reason accepts only wrong_amount, wrong_counterparty, wrong_period and other. Any other value is stored as null rather than refused: the reason is a help to the reader, not the data the call turns on.
Confirming: the convergence
This is where the money moves. The body carries expected_version and the complete list of intended allocations.
curl -X POST https://app.scribee.tech/api/v1/bank_operations/30144/reconciliation?include=allocations,bank_accounting_entry \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Idempotency-Key: 9c2b7e51-3f44-4a20-8f6e-1d0b7a4c3e92" \
-H "Content-Type: application/json" \
-d '{
"expected_version": 6,
"allocations": [
{ "invoice_document_id": 90210, "allocated_amount": "200.00" },
{ "invoice_document_id": 90211, "allocated_amount": "100.00" }
]
}'
The response is the recomputed operation - the whole bank_operation resource, described in Bank operations. With ?include=allocations,bank_accounting_entry you read the confirmed set and the balanced accounting lines back in the same round trip, instead of trusting the 200: a 300.00 movement split 200/100 comes back as two allocations and three lines, never as one mutated line.
A few body rules:
allocationsmust be a non-empty list. To undo everything, use theDELETE.- An invoice can only be named once. Two entries on the same invoice are refused rather than added together: a partner splitting 200 and 100 across the same invoice means 300, and guessing which of the two readings they wanted would be worse than telling them the body is malformed.
- One unreadable entry makes the whole list unreadable, it is not dropped. This is an all-or-nothing body.
- The set's total is measured against the absolute value of the movement before anything is written; going over is
allocation_exceeds_operation. - An amount that could only be settled in part refuses the whole call, it is not quietly reduced.
That last point deserves an example, because it happens on a body the movement's ceiling accepts. On a 300.00 operation you ask for A = 200.00 and B = 100.00, but invoice B owes only 50.00. The requested total is 300.00 and so fits inside the movement; it is the invoice that cannot take what you allocated to it. The call is refused and nothing is written, instead of confirming A = 200.00 and B = 50.00 - a set you did not ask for and would only discover by re-reading the allocations. The message names the three figures you need: the invoice, the amount requested, and the amount that would have been settled instead. Send back the amount the invoice can take, or allocate the remainder elsewhere.
The same refusal applies to a four-decimal amount that would settle nothing: allocations are posted to the cent on their running total, so an amount that, added to what the operation already carries, does not reach another cent - 0.0001 on an operation with no allocation, or 0.0001 after 33.3333 - would post no payment. The call is refused the same way and nothing is written; the message gives zero as the amount that would have been settled. Here, unlike the usual case it mentions, the invoice does not owe less than the amount sent: it is the amount that does not reach the cent.
Idempotency-Key and expected_version answer two different questions
This is what an integration gets wrong most easily, and the mistake only shows at the worst moment.
| What it protects against | What it does not do | |
|---|---|---|
Idempotency-Key | The same request arriving twice - a network that drops, a client that retries | It does not look at the operation's state. It will replay your stored body without ever checking that the allocation set has not moved in the meantime |
expected_version | A stale request - computed from a picture of the allocation set that someone else has since moved | It deduplicates nothing. Two different requests at the same version both go through, the second against the version the first wrote |
Neither substitutes for the other, and the confirmation requires both. A partner who sends an Idempotency-Key assuming it protects them from a stale version is wrong exactly at the moment a colleague has just changed the split from the screen.
version is read on the operation. It moves on every change to the allocation set, including the ones that leave reconciliation_status where it was: a second allocation on an operation already partially_matched does not move the status but does move the version.
Every write on the allocations resource moves it - drafting, re-sizing, withdrawing. So re-read version after every POST, PATCH or DELETE on allocations: a confirmation computed before that write carries a version that is now stale and will be refused with a 409. That is exactly what it is for - without it, two clients editing the same split would overwrite each other in silence - but it is worth knowing so you do not read it as a fault.
expected_version is compared as an integer
It is accepted as a JSON string or as a number - "6" and 6 are read the same way - but it is compared as an integer. A value that is neither an integer nor a string of decimal digits is refused 422 on the expected_version field, and not 409: it is not a stale version, it is a value we cannot read.
The distinction matters for your error handling: a 409 is retried at a fresher version, while a 422 on this field will never succeed on retry as long as the value you send keeps that shape.
The 409 carries the current state
{
"error": "conflict",
"code": "stale_version",
"message": "Cette opération bancaire a changé depuis sa lecture ; rien n'a été écrit. La version courante et le jeu d'affectations figurent dans details.",
"details": {
"current_version": 8,
"operation": {
"id": 30144,
"amount": "300.0000",
"direction": "incoming",
"reconciliation_status": "partially_matched"
},
"allocations": [
{
"id": 7001,
"invoice_document_id": 90210,
"allocated_amount": 200.0,
"status": "confirmed"
}
]
}
}
Nothing was written. details carries the current state, not the one you sent: re-render from it and retry with details.current_version, with no second round trip.
Two things to know about this body:
details.operation.amountis the four-decimal string thebank_operationresource publishes, never a number. So the two readings cannot diverge.details.allocationsholds only the active allocations, ordered by id. Rejections are excluded: they are not part of the picture you should have confirmed against.
This is the only 409 on this API that carries a details. idempotency_key_reuse and idempotency_request_in_progress also answer 409 on this path, with no details.
Two confirmations touching the same invoice can be recorded at the same moment, from two different operations. When the race leaves the losing operation with no active allocation, it receives this 409 stale_version; otherwise it receives the 422 refusal that the invoice's new state calls for - for example an invoice already settled. If the collision happens again, it receives a 422 operation_failed. In both cases nothing was written: read the bank operation again, then send the reconciliation again if it still applies.
entry_already_exported is a 422, deliberately
Once the operation's accounting entry has left the books - exported to an accounting package, or locked by an export run - no allocation change can rewrite it any more.
This refusal is a 422 and not a 409, and that is not an oversight. A 409 says the request is stale and that a retry at a fresher version can succeed; an exported entry makes the request impossible at any version. The remedy is a reclassification entry, never a rewrite.
The same refusal applies on the DELETE.
reconfirmation_out_of_order: a more recent allocation stays confirmed
The operation's accounting entry splits the confirmed allocations to the cent, on their running total, in the order of their ids. A confirmation, for its part, posts its payment after those of the allocations already confirmed, and those payments are never re-posted. When you confirm an allocation while a more recent allocation of the same operation - one with a higher id - stays confirmed, and the payments would then differ from the entry's split, the confirmation is refused with a 422 reconfirmation_out_of_order. Nothing was written: the whole set rolls back, including the withdrawals the call had already made.
This refusal can only occur on an operation where an allocation has, or had, an amount that goes beyond the cent - a third or a fourth decimal. When every amount is in whole cents and every confirmed allocation's payment equals its amount, it never occurs. The two typical cases:
- sending again an allocation withdrawn earlier, while a more recent allocation stayed confirmed;
- sending an allocation again at a different four-decimal amount, while keeping a more recent allocation.
details.allocations names, by invoice_document_id, the invoice you were confirming and those of the more recent allocations.
Scribee does not withdraw those more recent allocations for you. To proceed, first send the set without them: they are withdrawn, the rest is confirmed, and the response carries the new version. Then send the complete set again with that version: the allocations are then confirmed in the order of their ids.
This refusal too is a 422 and not a 409: a retry at a fresher version would fail the same way. Payments already posted are never re-posted, so the more recent allocations have to come off first.
Undoing a confirmation
curl -X DELETE https://app.scribee.tech/api/v1/bank_operations/30144/reconciliation \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"allocation_ids": [7001]}'
The payments of the named allocations are destroyed, the accounting entry's suspense line is restored, and the response is the recomputed operation - with the same includes as the confirmation.
Each allocation is independently undone. Name allocation_ids and the others are left exactly where they are; omit the key entirely and every confirmed allocation comes off. An empty list is refused rather than read as "all": the two readings are opposite, and a partner whose filter produced an empty list meant the first one.
An undone allocation becomes unconfirmed, not rejected. If you meant to refuse it for good, follow up with DELETE /api/v1/bank_allocations/{id}.
expected_version is optional here and required on the confirmation. That is not an oversight: a confirmation is computed from a picture of the allocation set and becomes wrong if that picture moved, whereas an undo names the rows it wants by id. When you do supply it, it is enforced identically.
An id that is not a confirmed allocation of this operation is refused, not silently ignored - including one belonging to another operation. A partner who sent it believed it was settled, and a silent success would tell them it had been undone.
This is also the correct way to remove an invoice payment that came from a reconciliation. DELETE on /api/v1/invoices/{invoice_id}/payments/{id} refuses a payment carrying a bank operation: detaching the money from the reconciliation that created it would leave the ledger describing a settlement that no longer exists. PATCH on the same path only accepts note on such a payment; a request carrying any other field, even with its current value, is refused whole (Correct a payment).
An operation that settles a payment batch
When an operation has been confirmed as the settlement of an instruction of a payment batch (Payment batches), that confirmation stands on its allocations. While that link stands, the operation's allocations change only by undoing that settlement, and the following calls are refused with a 422 operation_failed:
DELETE /api/v1/bank_operations/{id}/reconciliation, whatever allocation is named;POST /api/v1/bank_operations/{id}/reconciliationwhen the set sent would withdraw a confirmed allocation - by omitting it, or by sending it again at another amount;POST /api/v1/bank_operations/{id}/reconciliationwhen it would confirm the operation against an invoice that is not the instruction's. An instruction that pays no invoice admits none;PATCH /api/v1/bank_allocations/{id}carryingallocated_amount.
details.base says so and names the step to take. Nothing was written.
To proceed, undo the settlement first with POST /api/v1/payment_batches/{id}/settlement/instructions/{payment_instruction_id}/undo (Undoing a confirmed settlement). That undo removes the link and only what the confirmation had recorded; the operation's allocations can then be reworked here like any others.
Listing allocations
curl https://app.scribee.tech/api/v1/bank_operations/30144/allocations \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
The list is ordered by created_at ascending, oldest first - the order the split was built in - and ties are broken by ascending id.
No status filter is applied by default, and rejections are returned too. That is deliberate: without them you cannot tell an invoice that was never proposed from one that was proposed and then refused, and the refusal is precisely the audit record.
| Parameter | What it narrows to |
|---|---|
status | One allocation state |
invoice_document_id | The allocation against a given invoice |
include | invoice_document, bank_operation, comma-separated |
An unrecognised value narrows nothing and raises no error - it returns an empty page, as everywhere else on this surface. An include value that is neither of the two is ignored, and without include those keys are absent from the response rather than published as null.
Embeds do not nest: bank_operation is returned to you without its own includes, so an allocation cannot come back to you inside itself.
page and per_page paginate as everywhere else (20 by default, 100 maximum).
Errors
| Code | When |
|---|---|
400 | page is not an integer greater than or equal to 1, or it names a page beyond the last one |
401 | No token, or a token that is invalid or expired |
403 | Your token does not carry the scope the verb expects, or bank reconciliation is switched off for the workspace concerned |
404 | No operation - or no allocation - with this id is reachable by your grants |
409 | stale_version, or one of the two Idempotency-Key conflicts |
422 | The body or the resource's state refuses the call. code says which |
The code values you will meet here - the first four are written nowhere else on the API:
code | Meaning |
|---|---|
stale_version (409) | The version you sent is no longer the operation's. Nothing was written; details carries the current state |
allocation_exceeds_operation (422) | The allocation set would settle more than the movement carries |
entry_already_exported (422) | The operation's accounting entry has left the books and can no longer be rewritten |
reconfirmation_out_of_order (422) | The confirmation would post the rounding cent in a different order from the entry, because a more recent allocation stays confirmed. Nothing was written; send the set without it first, then the complete set |
operation_failed (422) | Another reconciliation touching the same invoices was being recorded at the same moment, and the collision happened again; or the operation settles an instruction of a payment batch, and that settlement must be undone first. Nothing was written |
validation_failed (422) | Everything else: a malformed field, an invoice not found, a domain rule refused, a state that can neither be withdrawn nor re-sized, an amount an invoice cannot take in full |
The scope is checked before the resource exists. A token without the expected scope gets 403 on a nonexistent id exactly as on a valid one. On the resources themselves, though, reachability is settled before the feature: an operation your token cannot read answers 404, never 403. So none of these responses can be used to probe for an id.