Upload bank statements
POST/api/v1/workspaces/:workspace_id/companies/:company_id/bank_statements
Uploads one or more statement files into a company. One file, one statement, so this 202 answers a COLLECTION - one row per file, in request order, each with its own account, state and error - and you poll N ids rather than one. A file that fails extraction never takes its siblings down with it. A part naming an account you cannot reach is different: that is a malformed request, so the whole upload is refused and nothing is stored.
The extraction sends the statement to Mistral AI
In extract mode - the default when the mode part is omitted - the uploaded file is made available to Mistral AI, a third-party OCR provider, for reading. State it to whoever uploads through you before they send a bank statement: these are financial records, and the decision to have them read by an external processor is theirs to make knowingly.
The flow, exactly: the extraction job generates a signed, expiring URL to the stored file and posts that URL to Mistral's OCR endpoint (https://api.mistral.ai/v1/ocr); Mistral fetches the document from it and returns the lines and balances this API then publishes. The URL is signed for 5 minutes, which bounds how long it can be fetched from - it says nothing about what the provider does with what it already read. Scribee publishes no statement here about Mistral's retention or processing location; if your own processing record needs those, ask us rather than infer them from this page.
Two ways the call does NOT reach Mistral. mode=archive stores the file and nothing else - no extraction is queued and nothing leaves the platform; use it whenever you want retention without reading. And AI extraction is a per-company setting that is off unless it was turned on in Scribee: with it off, an extract upload is accepted and stored, no extraction runs, and the statement simply stays pending - so a 202 is not on its own evidence that anything was sent anywhere.
Request
Responses
- 202
- 401
- 403
- 404
- 409
- 422
The files were accepted. Each row exists and is yours to poll; none of them has been extracted yet.
Check for meta.unqueued_statement_ids before you start polling. It is absent from the ordinary upload, and present when the statements were stored but one or more extractions could not be started. Those ids will sit at pending with nothing reading them until you re-drive each one with POST /bank_statements/{id}/retry. Retry exactly those: the rest are queued, and re-driving them pays for a second extraction. Do NOT upload the files again - the statements in data already exist, and a fresh upload duplicates them. Replaying this Idempotency-Key is safe and returns this same body with Idempotency-Replayed: true; it creates nothing.
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 company with this id exists in the workspace named in the path, so nothing was stored. A company belonging to ANOTHER workspace answers the same way - even when your token also holds a grant for that one - so the response cannot be used to probe for a company. Resolved before the action runs, which is why it precedes every refusal above, the Idempotency-Key ones included: a key spent on this call is not claimed.
The Idempotency-Key was already used for a DIFFERENT upload - idempotency_key_reuse. Nothing was stored by this call. Send a fresh key for a new upload, or resend the ORIGINAL files under the same key to replay the stored 202.
The same status carries idempotency_request_in_progress for a different situation: the key is claimed and its outcome is not replayable. That is USUALLY a first call still running, but it is also what you get when a first call completed its effect and then failed while rendering its response - so it does NOT mean nothing happened, and the two cases are not distinguishable from the outside today. Do not retry in a loop. List the company's statements and see what the first call actually created before doing anything else; re-uploading would duplicate whatever it did. The key frees itself 24 hours after first receipt.
The upload was refused and details names the field. file when no part carried one, or when a part sent it as a text value, an object or a list instead of an uploaded file part, mode when the parts disagreed about it - one upload is one batch - or when a part sent it as something other than a mode name, the empty value included: mode= is refused rather than read as the default, because defaulting it would start an extraction nobody asked for. bank_account when a part named an account this company cannot reach. Nothing was stored in any of those cases.
Every refusal on this operation is validation_failed, and none of them stored anything - a 422 here always means the upload was rejected whole, so fix the named field and send it again, with the same Idempotency-Key if you like.
A partial enqueue is NOT one of these. The files are accepted and stored, so it answers 202 with the collection and a meta.unqueued_statement_ids naming the rows whose extraction could not be started - see the 202 above.