---
title: "Read a statement and put its movements on an account"
method: POST
path: "/api/v1/banking/accounts/{accountId}/statements"
tags: ["Portfolio"]
---

# Read a statement and put its movements on an account

`POST /api/v1/banking/accounts/{accountId}/statements`

## Request

- Base URL: `http://localhost:5180`
- URL: `http://localhost:5180/api/v1/banking/accounts/{accountId}/statements`
- Auth: API key in cookie `better-auth.session_token`

Takes the statement as `file` in a multipart form, or a PDF one as `page` image parts in order. A PDF sent as `file` is filed with the account and read in the background, and the answer carries `processing` rather than lines: its import stays `processing` until the movements are on the account. CODA and CAMT are specified formats and are read exactly; a CSV or a spreadsheet whose header names its columns is read exactly too; anything else is read by a model, and `read` says which of the three happened. A movement already on the account, including one your bank reported before the file arrived, is returned marked `duplicate` and is never written twice. Send `dryRun=true` to see what would be imported without importing it, which is what the screen does before it asks.

Each line carries a `hash`. Send the hashes of the lines the owner picked as `lines` with the real import and only those are written, so a line unticked in the preview stays out. Leave `lines` out and every line not already held is written.

Once new movements are written, incoming money Dardaris is sure of is placed against what it pays, under the owner’s placement setting. History is never placed. `placed` and `waiting` say what happened; `waiting` is null when placing is left to the hourly pass.

## Path parameters

- `accountId` string, required

## Response `200`

What the file said, what was written, and what was placed.

- object
  - `ok` unknown, required
  - `data` object, required
    - `format` 'coda' | 'camt' | 'csv' | 'xlsx' | 'pdf' | 'unknown', required
    - `read` 'standard' | 'columns' | 'model', required: How the figures were arrived at: `standard` from CODA or CAMT, `columns` from a header row that named them, `model` from a reading of a layout nobody has written down.
    - `lines` object[], required
      - `date` string, required: A calendar date, `YYYY-MM-DD`.
      - `valueDate` string: A calendar date, `YYYY-MM-DD`.
      - `amount` integer, required: The size of the movement in cents, always positive. `direction` says which way it went.
      - `direction` 'in' | 'out', required
      - `description` string, required
      - `counterpartyName` string
      - `counterpartyIban` string
      - `structuredRef` string
      - `raw` string, required: The row exactly as it arrived, so a wrong reading is traceable to its source.
      - `hash` string, required: The same for the same movement on every read of the same file. Send the ones the owner picked back as `lines` and only those are imported.
      - `duplicate` boolean, required: Already on the account. Shown and never imported.
    - `skipped` number, required
    - `duplicates` number, required
    - `message` string, required
    - `account` object, required
      - `iban` string
      - `currency` string
      - `holder` string
    - `mismatch` string: Set where the file names a different account from the one it was uploaded into. The import still runs; the owner is told.
    - `trouble` 'unavailable' | 'unreadable' | 'ran_long'
    - `imported` object, nullable, required
      - `statementImportId` string, required: Stable record id, e.g. `lse_012`.
      - `transactions` number, required: How many movements were written. Duplicates and skipped rows are not among them.
    - `placed` integer, required: Incoming movements placed against what they pay once this import landed, under the owner’s placement setting. 0 when nothing new was written.
    - `waiting` integer, nullable, required: Incoming movements still waiting for the owner after that. Null when placing did not run on this request: a dry run, nothing new written, or placing failed after the movements had landed, in which case the hourly pass looks at them.
    - `layout` object: For a CSV or a spreadsheet: which column was read as which field (an index into `headers`, -1 for none), the date order and the decimal mark, with the first rows under the header. Send it back, corrected, as a JSON `layout` field and the file is read exactly that way, on the dry run and on the import alike.
      - `headerRow` number, required
      - `columns` object, required
      - `dateOrder` 'dmy' | 'mdy' | 'ymd', required
      - `decimal` ',' | '.', required
      - `headers` string[], required
      - `sample` array[], required
        - string[]
    - `processing` object: Set when a PDF was sent as `file`. It is filed and read in the background, and the statement import stays `processing` until its movements are on the account, or turns `failed` with a reason. `alreadyImported` is true where this very file was already imported into the account, and nothing new was started.
      - `statementImportId` string, required: Stable record id, e.g. `lse_012`.
      - `alreadyImported` boolean, required
  - `meta` object, required
    - `requestId` string, required: Echoed in the x-request-id header. Quote it in a bug report.

## Other responses

- `400`: No file, a file over the ceiling, or nothing in it that reads as a statement.
- `401`: No session. Sign in first.
- `404`: No such bank account on this team.
- `409`: A picked line is not in this reading of the file, so nothing was imported.
- `429`: Over the upload or the reading budget.

---

[API](https://skmtc.dev/dardaris/apis/dardaris-api.md) · [All operations](https://skmtc.dev/dardaris/apis/dardaris-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dardaris/apis/dardaris-api/revisions/4aff33835c73?raw)
