---
title: "Upload Bank Statement"
method: POST
path: "/companies/{company_id}/bank-statements"
tags: ["reconciliation"]
---

# Upload Bank Statement

`POST /companies/{company_id}/bank-statements`

## Request

- Base URL: `https://app.zeno-works.ch` (the document declares no server; this is the origin it was published from)
- URL: `https://app.zeno-works.ch/companies/{company_id}/bank-statements`
- Auth: none declared

Dual-mode bank-statement upload (api-contract §Reconciliation).

Single file: CAMT.053 **XML** → synchronous parse/persist/match (201); **PDF/CSV/
Excel** → stage by sha256 + create a ``BankStatementImport`` (202, async worker).
A **ZIP** is expanded and routed per leaf (XML leaves parse sync, others queue) and
the response aggregates both arms. Status is **201** when any synchronous CAMT work
happened, else **202** (purely queued). 413 over the cap, 422 on bad CAMT / bad zip /
unsupported single-file type.

## Path parameters

- `company_id` string, required

## Headers

- `authorization` string, nullable

## Cookies

- `access_token` string, nullable

## Response `201`

Successful Response

- StatementUploadResponse
  - `msg` string, required
  - `statements` BankStatementOut[], required
    - `id` string, required
    - `company_id` string, required
    - `bank_account_id` string, required
    - `statement_identifier` string, required
    - `sequence_number` integer, nullable
    - `legal_sequence_number` integer, nullable
    - `from_date` string, date-time, nullable
    - `to_date` string, date-time, nullable
    - `created_datetime` string, date-time, nullable
    - `opening_balance` string, nullable
    - `opening_balance_date` string, date, nullable
    - `closing_balance` string, nullable
    - `closing_balance_date` string, date, nullable
    - `currency` string, nullable
    - `entry_count` integer, required
    - `source_filename` string, nullable
    - `source_sha256` string, nullable
    - `imported_at` string, date-time, required
    - `withdrawn` boolean
  - `entries_created` integer, required
  - `duplicates_skipped` integer, required
  - `possible_duplicates` integer, required
  - `already_imported` boolean, required
  - `skipped` StatementSkippedEntry[]
    - `name` string, required
    - `reason` string, required
  - `held_accounts` HeldAccountOut[]
    - `id` string, required
    - `iban` string, required
    - `holder_name` string, nullable
    - `bank_name` string, nullable
  - `queued` BankStatementImportOut[]
    - `id` string, required
    - `company_id` string, required
    - `sha256` string, required
    - `filename` string, required
    - `content_type` string, nullable
    - `size` integer, nullable
    - `uploaded_by` string, nullable
    - `status` 'pending' | 'processing' | 'done' | 'failed', required: Async-processing lifecycle of an uploaded non-CAMT bank statement. Mirrors :class:`AccountingDocumentStatus` (its own dedicated Postgres enum type ``bank_statement_import_status``): ``pending`` (enqueued) → ``processing`` (claimed by the ``BankStatementImportWorker``) → ``done`` / ``failed``.
    - `error` string, nullable
    - `result_summary` object, nullable
    - `statements_created` integer, required
    - `entries_created` integer, required
    - `created_at` string, date-time, required
    - `processed_at` string, date-time, nullable
    - `fault` IngestFaultOut: Why an ingest record failed or is parked, and what removes the cause.
      - `code` 'transient' | 'quota_held' | 'defect_fixed' | 'platform_defect' | 'bytes_gone' | 'too_large' | 'unreadable' | 'unexplained' | 'unsupported_type' | 'parse_error' | 'format_unknown' | 'not_a_statement' | 'no_account_id' | 'nothing_readable' | 'no_statement_found' | 'fiscal_year_undeterminable' | 'corrupt_archive' | 'empty_archive' | 'ambiguous_tabular' | 'analysis_failed' | 'statement_format_not_importable' | 'statement_beside_invoice' | 'statement_beside_card_bill' | 'accounting_document_not_importable' | 'statement_import_failed' | 'archive_incomplete' | 'encrypted' | 'corrupt' | 'limit_exceeded' | 'ratio' | 'depth_exceeded' | 'empty' | 'archive_no_connection' | 'archive_connection_disabled' | 'archive_reconnect_required' | 'archive_no_template' | 'archive_upload_failed' | 'triage_unrelated' | 'triage_needs_review' | 'triage_irrelevant', required: The closed vocabulary. The SPA has exactly one row per member (a test enforces it).
      - `retry` 'auto' | 'user' | 'none', required: Whether running the same operation again can change the outcome.
      - `actions` IngestFaultAction[], required
      - `params` object

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/zeno-works/apis/zeno-api.md) · [All operations](https://skmtc.dev/zeno-works/apis/zeno-api/llms.txt) · [OpenAPI document](https://skmtc.dev/zeno-works/apis/zeno-api/revisions/1eb93dde146c?raw)
