Import a bank statement
The bank_statements.imports.create operation: POST /api/v1/bank_statements/imports in the REST API and the bank_statements_imports_create tool in the MCP server.
/api/v1/bank_statements/importsImports a bank statement of the legal entity's account as CSV and matches its incoming transfers to open invoices of every brand of the entity (send_invoice invoices carry their number as the payment reference).
- The file needs a header row and, per booked transaction, a booking date, a signed amount (incoming money is
positive), the reference and optionally the counterparty and a currency. Columns are recognised by common
header names (English, German, Croatian) or named in
columns; dates asYYYY-MM-DD,DD.MM.YYYYor with slashes (dateFormatrequired);decimalSeparator.or,. A file with an unreadable row is refused whole (bank_statement_invalid, rows indetails.errors); at most 2,000 transactions per file. - The same file twice is refused; lines an earlier import has (overlapping statements) are skipped and
counted as
duplicates. - A line whose reference names exactly one open invoice and whose amount is that invoice's amount due pays
it at once, as
invoices.mark_paidwould on the booking date (invoice.paid; a past-due subscription recovers). Every other line waits in the review queue (bank_statements.lines.listwithmatchStatus=partial,unmatched) with the reason and the invoices it may pay.
Financial tier: it records payments. Dry-run it first to see the matches without saving anything. CAMT.053 files arrive in a later version.
At a glance
- REST:
POST /api/v1/bank_statements/imports - MCP tool:
bank_statements_imports_create - Operation ID:
bank_statements.imports.create - Scope: Legal entity
- Permission:
invoices:write - Risk tier:
financial - Dry run: Supported (send
Ownrecord-Dry-Run: true) - Idempotency:
Idempotency-Keyrequired - Modes: Test and live
- Authentication: API key with the
invoices:writescope - Since: M5
Request
Body
A JSON object (Content-Type: application/json):
| Field | Type | Required | Description |
|---|---|---|---|
csv | string | Yes | The statement as CSV text: a header row, then one booked transaction per row (booking date, signed amount, reference, optionally counterparty and currency). |
currency | string | Yes | The bank account's currency (rows without a currency column). |
filename | string | No | |
delimiter | one of ,, ;, | No | ,, ; or a tab. Default: detected from the header. |
dateFormat | one of YYYY-MM-DD, DD.MM.YYYY, DD/MM/YYYY, MM/DD/YYYY | No | Default: YYYY-MM-DD, or DD.MM.YYYY when the dates have dots. Dates with slashes need it. |
decimalSeparator | one of ., , | No | Default: , in ;-separated files, else .. |
columns | object | No | Header names to use when the recognised ones don't fit (e.g. { reference: "Remittance" }). |
columns.bookingDate | string | No | |
columns.amount | string | No | |
columns.reference | string | No | |
columns.counterparty | string | No | |
columns.currency | string | No |
Response
201 Created with a JSON object:
| Field | Type | Description |
|---|---|---|
id | bsi_… ID | |
object | "bank_statement_import" | |
entityId | ent_… ID | |
livemode | boolean | |
format | "csv" | |
filename | string or null | |
currency | string | |
lineCount | integer | Transactions in the file. |
counts | object | Lines by match status (kept current as lines are matched or ignored); duplicates were skipped. |
counts.imported | integer | |
counts.duplicates | integer | |
counts.matched | integer | |
counts.partial | integer | |
counts.unmatched | integer | |
counts.ignored | integer | |
from | string or null | |
to | string or null | |
options | object | |
options.delimiter | one of ,, ;, | |
options.dateFormat | one of YYYY-MM-DD, DD.MM.YYYY, DD/MM/YYYY, MM/DD/YYYY or null | |
options.decimalSeparator | one of ., , or null | |
options.columns | object | |
options.columns.bookingDate | string or null | |
options.columns.amount | string or null | |
options.columns.reference | string or null | |
options.columns.counterparty | string or null | |
options.columns.currency | string or null | |
importedBy | object | |
importedBy.type | string | |
importedBy.id | string | |
version | integer | |
createdAt | date-time | |
updatedAt | date-time | |
paidInvoiceIds | array of inv_… ID | The invoices this import paid automatically. |
Examples
Import a statement: one transfer pays its invoice, two wait for review
POST /api/v1/bank_statements/imports
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json
{
"csv": "Booking date,Amount,Reference,Counterparty\n2026-11-04,1200.00,GEN-2026-000131,Alpen Apps GmbH\n2026-11-04,450.00,Invoice 2026-000132,Berg Software AG\n2026-11-04,-12.50,Account fee,Bank",
"currency": "EUR",
"filename": "statement-2026-11-04.csv"
}
Response (201):
{
"id": "bsi_01m4tx5nj8emwbyf7y9vcf48ba",
"object": "bank_statement_import",
"entityId": "ent_01m4tx5nj8emwbyf7y9vcf47zm",
"livemode": false,
"format": "csv",
"filename": "statement-2026-11-04.csv",
"currency": "EUR",
"lineCount": 3,
"counts": {
"imported": 3,
"duplicates": 0,
"matched": 1,
"partial": 0,
"unmatched": 2,
"ignored": 0
},
"from": "2026-11-04",
"to": "2026-11-04",
"options": {
"delimiter": ",",
"dateFormat": null,
"decimalSeparator": null,
"columns": {
"bookingDate": "Booking date",
"amount": "Amount",
"reference": "Reference",
"counterparty": "Counterparty",
"currency": null
}
},
"importedBy": {
"type": "user",
"id": "usr_01m4tx5nj8emwbyf7y9vcf47zk"
},
"version": 2,
"createdAt": "2026-11-05T09:00:00.000Z",
"updatedAt": "2026-11-05T09:00:00.000Z",
"paidInvoiceIds": ["inv_01m4tx5nj8emwbyf7y9vcf47zn"]
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
validation_failed | 400 | A field is malformed or out of range. | Fix the fields listed in details.issues and send the request again. |
bank_statement_invalid | 400 | The CSV can't be read: a missing column, a bad date or amount (details.errors lists the rows). | Fix the rows listed in details.errors (or name the columns, date format and decimal separator) and import the whole file again. Nothing was imported. |
bank_statement_already_imported | 409 | This exact file was imported before (details.import_id). | Open the earlier import (details.import_id) instead. Overlapping statements with different files are fine: their common lines are skipped. |
version_conflict | 409 | An invoice or line changed at the same time (another import or payment); retry. | Fetch the object again and retry the change. |
permission_denied | 403 | The credentials can't record invoice payments for this entity (invoices:write, finance). | Ask an organization owner or admin for a role or scope that allows this action. |
It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, scope_required, resource_not_found, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, approval_required, approval_rejected, approval_expired, stale_approval, approval_policy_denied, rate_limited, internal_error.