Review a statement's lines and balances
PATCH/api/v1/bank_statements/:id/lines
Corrects extracted lines and, in the SAME call, the statement's two balances - there is no PATCH /bank_statements/{id}, and correcting a balance is the same review act as correcting a line. The submission is atomic: one malformed figure refuses all of it and writes nothing, so you never end up with a half-edited statement behind a 422.
The response is the reviewed lines, paginated like the GET, plus a meta carrying the statement's balances and reconciliation_errors - a warning on a 200, telling you the statement no longer adds up even though the correction was applied.
Which states accept a review: completed, partial and reviewing - the extraction is over and the file is still open. posted does not: the commit is done, and correcting a line afterwards would invalidate a ledger entry that already exists. pending, processing, failed and archived are not reviewable either, for four different reasons. pending means only that the file is stored and not yet reviewable - never that an extraction is running; processing is the one state that does mean work is under way. failed may still CARRY the lines of an earlier pass - a retry keeps them and a later failure does not delete them - so GET /bank_statements/{id}/lines will serve them, but they cannot be corrected until a successful extraction returns the file to completed or partial. archived was stored without running an extraction at all, so it has nothing to review and never will. Anything outside the three accepted states is refused statement_not_reviewable.
Inside an accepted state, both corrections are authorized in full: any of the statement's lines that is not yet reconciled or posted, and either of the two balances. Opening a review (reviewing) narrows nothing - it records that a human has the file, and further corrections are still taken.
A correction that ends a statement's review returns it to the state its extraction left it in - partial when the extraction reported errors, completed otherwise - so a corrected partial statement stays partial and stays retryable.
Request
Responses
- 200
- 400
- 401
- 403
- 404
- 409
- 422
The corrections were applied. The reviewed lines come back paginated, with the statement's balances and a freshly recomputed reconciliation_errors alongside them.
The page query parameter is not an integer greater than or equal to 1, or it names a page beyond the statement's last one. NOTHING was corrected - the page is answered before the review is applied, so this is safe to resend with a valid page.
The request carries no bearer token, or one that is invalid or expired. doorkeeper_authorize! is the first gate of the chain, so this is answered before the workspace, the company, the feature flag and the statement are ever resolved - and before any Idempotency-Key is claimed.
The token may not perform this call, and three gates of the shared chain answer it identically: the OAuth SCOPE derived from the HTTP verb (this one needs write), the workspace GRANT the application holds, and the bank_reconciliation FEATURE FLAG. The action's own Pundit check is a fourth. All four render the same body, so message is what distinguishes them; nothing was performed in any case. The example below is refused on the scope.
No statement with this id is reachable by the token's grants, so nothing was corrected. One belonging to another workspace answers the same way, so the response cannot be used to probe for one.
You sent the optional Idempotency-Key and it was already used for a DIFFERENT review body - idempotency_key_reuse. Nothing was corrected by this call. This is the conflict the review can actually produce, because unlike the delete it carries a body: two submissions under one key are two different corrections, and the platform refuses rather than guessing which you meant. Send a fresh key, or resend the ORIGINAL body to replay the stored 200. A first call still in flight answers the same status with idempotency_request_in_progress.
The submission was refused and NOTHING was written - the whole review is atomic. The body takes one of two shapes and you have to handle both, which is why this response is a oneOf rather than a single object.
No details - the refusal is about the submission or the statement as a whole, and code is what to branch on. validation_failed covers a malformed figure (a thousands separator, a fifth decimal, an out-of-range amount), a lines entry naming a line of another statement or carrying no id at all, and a LINE whose own state forbids correction - already reconciled or posted to the ledger, archived, or still pending. statement_not_reviewable covers the STATEMENT's state, which is answered before anything is written.
With details - the submission was malformed in a way that names the value to resend, and every malformed value is named at once. lines when it is not a list of line objects, or carries more entries than one submission allows; opening_balance / closing_balance when a balance arrived as an object, a list or a boolean; lines[N][field] - the index being the position you sent - when one field of one line did, with label narrower than the rest (text only). The code is validation_failed throughout.