Commit a reviewed bank statement
POST/api/v1/bank_statements/:id/commit
Posts the statement's extracted lines to the general ledger and closes its review. The 200 returns the statement at posted, with committed true.
Your POST is the confirmation, for the whole upload. The platform will not post figures nobody has taken responsibility for, so this call carries that assertion: you have read the lines (GET /bank_statements/{id}/lines), corrected what needed it (PATCH /bank_statements/{id}/lines), and you are committing them. There is no separate 'confirm review' call to make first - and because the commit posts the whole upload, the confirmation this POST carries covers every file of it, not only the one in the path.
THE UNIT IS THE UPLOAD, NOT THE FILE. Every statement created by one multipart POST belongs to one batch, and a batch is posted whole or not at all. So committing any file of an upload commits ALL of them - and a single sibling still owing work refuses the whole call with batch_unresolved, having written nothing. Commit once per upload, not once per file; a second call naming a sibling finds the work already done.
Nothing is partially written. The whole commit runs in one transaction, so every 422 below leaves the ledger exactly as it was and the same Idempotency-Key may be resent once you have fixed what was refused.
One 4xx is NOT a rollback, and it is the reason to send the header. If this platform fails while building the response, the commit has already landed and you may receive a 4xx over accounting that IS in the ledger. The key is what closes that: retrying it answers 409 idempotency_key_consumed, telling you the first call finished and posted. Read GET /bank_statements/{id} - state: posted and committed: true mean the work is done. Re-committing is safe in any case: a posted file is answered 200 and posts nothing again.
Request
Responses
- 200
- 401
- 403
- 404
- 409
- 422
The upload was posted. The statement comes back at posted, with committed true.
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 statement, the feature flag and any Idempotency-Key are resolved - and before anything is posted.
The token may not perform this call, and four gates answer it identically: the OAuth SCOPE derived from the HTTP verb (this one needs write), the workspace GRANT the application holds, the bank_reconciliation FEATURE FLAG, and the action's own Pundit check. All four render the same body, so message is what distinguishes them; nothing was posted in any case. The example below is refused on the scope.
No statement with this id is reachable by the token's grants. One belonging to another workspace answers the same way, so the response cannot be used to probe for one.
This Idempotency-Key is claimed and its outcome is not replayable. Nothing was posted by THIS call, and the two codes differ in what to do next. idempotency_request_in_progress: a first call is still running - read GET /bank_statements/{id} and let state tell you whether it reached posted, rather than looping. idempotency_key_consumed: the first call FINISHED and posted, then failed while building its response - nothing is running, so do not poll; confirm with GET /bank_statements/{id} and treat the upload as committed. A commit carries no body, so its fingerprint can never differ from the first call's and idempotency_key_reuse is unreachable here; a completed first call replays its stored 200 instead of conflicting. The key frees itself 24 hours after first receipt.
The commit was refused and NOTHING was posted. Branch on code, which is what tells you where the remedy is - the six are not interchangeable:
statement_not_reviewable- the file's own state forbids it: it is archived, or its extraction has not finished. PollGET /bank_statements/{id}untilstatemoves; correcting lines will not clear it. Nodetails. An already-posted file is NOT this refusal - it answers 200 with the posted row, so a retry after a timeout is always safe.statement_excluded- the file you named is excluded from its upload (excluded: true), so the commit leaves it out and will not post the rest of the upload in its name. Either commit by naming another file of the upload, or put this one back withPOST /bank_statements/{id}/includeand commit again. Polling never clears it. Nodetails.account_not_ready- the target bank account cannot receive accounting: no ledger attached, the account deactivated, or your bank no longer sharing it. The remedy is on the ACCOUNT, so polling the statement is pointless.batch_unresolved- the statement you named is committable, but another file of the same upload still owes work.messagesays how many anddetailssays WHICH, keyed<statement_id>.<reason>..not_reviewedmeans that file could not be opened for review - read itsstate:pendingorprocessingmeans let the extraction finish,failedmeans retry it. Any other suffix names a field of that file to correct through its ownPATCH /lines. Then commit again, once.validation_failed- THIS file's extraction is defective, anddetailsis keyed by the field to correct throughPATCH /bank_statements/{id}/lines. It also carriesdetails.idempotency_keywhen the required header is absent or outside 1-255 characters, which is refused in front of the action.posting_failed- the ledger itself refused the projection, or a confirmed match could not be imputed. The file is still committable, so this is the one refusal that is worth retrying unchanged once the accounting configuration is fixed.